Initial commit: Plane
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { Info } from "lucide-react";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// helpers
|
||||
import type { TAuthErrorInfo } from "@/helpers/authentication.helper";
|
||||
|
||||
type TAuthBanner = {
|
||||
bannerData: TAuthErrorInfo | undefined;
|
||||
handleBannerData?: (bannerData: TAuthErrorInfo | undefined) => void;
|
||||
};
|
||||
|
||||
export const AuthBanner: React.FC<TAuthBanner> = (props) => {
|
||||
const { bannerData, handleBannerData } = props;
|
||||
|
||||
if (!bannerData) return <></>;
|
||||
return (
|
||||
<div className="relative flex items-center p-2 rounded-md gap-2 border border-custom-primary-100/50 bg-custom-primary-100/10">
|
||||
<div className="w-4 h-4 flex-shrink-0 relative flex justify-center items-center">
|
||||
<Info size={16} className="text-custom-primary-100" />
|
||||
</div>
|
||||
<div className="w-full text-sm font-medium text-custom-primary-100">{bannerData?.message}</div>
|
||||
<div
|
||||
className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-custom-primary-100/20 text-custom-primary-100/80"
|
||||
onClick={() => handleBannerData && handleBannerData(undefined)}
|
||||
>
|
||||
<CloseIcon className="w-4 h-4 flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
// helpers
|
||||
import { EAuthModes } from "@/types/auth";
|
||||
|
||||
type TAuthHeader = {
|
||||
authMode: EAuthModes;
|
||||
};
|
||||
|
||||
type TAuthHeaderContent = {
|
||||
header: string;
|
||||
subHeader: string;
|
||||
};
|
||||
|
||||
type TAuthHeaderDetails = {
|
||||
[mode in EAuthModes]: TAuthHeaderContent;
|
||||
};
|
||||
|
||||
const Titles: TAuthHeaderDetails = {
|
||||
[EAuthModes.SIGN_IN]: {
|
||||
header: "Sign in to upvote or comment",
|
||||
subHeader: "Contribute in nudging the features you want to get built.",
|
||||
},
|
||||
[EAuthModes.SIGN_UP]: {
|
||||
header: "View, comment, and do more",
|
||||
subHeader: "Sign up or log in to work with Plane work items and Pages.",
|
||||
},
|
||||
};
|
||||
|
||||
export const AuthHeader: React.FC<TAuthHeader> = (props) => {
|
||||
const { authMode } = props;
|
||||
|
||||
const getHeaderSubHeader = (mode: EAuthModes | null): TAuthHeaderContent => {
|
||||
if (mode) {
|
||||
return Titles[mode];
|
||||
}
|
||||
|
||||
return {
|
||||
header: "Comment or react to work items",
|
||||
subHeader: "Use plane to add your valuable inputs to features.",
|
||||
};
|
||||
};
|
||||
|
||||
const { header, subHeader } = getHeaderSubHeader(authMode);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-2xl font-semibold text-custom-text-100 leading-7">{header}</span>
|
||||
<span className="text-2xl font-semibold text-custom-text-400 leading-7">{subHeader}</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
250
apps/space/core/components/account/auth-forms/auth-root.tsx
Normal file
250
apps/space/core/components/account/auth-forms/auth-root.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { SitesAuthService } from "@plane/services";
|
||||
import type { IEmailCheckData } from "@plane/types";
|
||||
import { OAuthOptions } from "@plane/ui";
|
||||
// assets
|
||||
import GiteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
|
||||
import GithubLightLogo from "@/app/assets/logos/github-black.png?url";
|
||||
import GithubDarkLogo from "@/app/assets/logos/github-dark.svg?url";
|
||||
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
|
||||
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
|
||||
// helpers
|
||||
import type { TAuthErrorInfo } from "@/helpers/authentication.helper";
|
||||
import { EErrorAlertType, authErrorHandler, EAuthenticationErrorCodes } from "@/helpers/authentication.helper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store/use-instance";
|
||||
// types
|
||||
import { EAuthModes, EAuthSteps } from "@/types/auth";
|
||||
// local imports
|
||||
import { TermsAndConditions } from "../terms-and-conditions";
|
||||
import { AuthBanner } from "./auth-banner";
|
||||
import { AuthHeader } from "./auth-header";
|
||||
import { AuthEmailForm } from "./email";
|
||||
import { AuthPasswordForm } from "./password";
|
||||
import { AuthUniqueCodeForm } from "./unique-code";
|
||||
|
||||
const authService = new SitesAuthService();
|
||||
|
||||
export const AuthRoot: React.FC = observer(() => {
|
||||
// router params
|
||||
const searchParams = useSearchParams();
|
||||
const emailParam = searchParams.get("email") || undefined;
|
||||
const error_code = searchParams.get("error_code") || undefined;
|
||||
const nextPath = searchParams.get("next_path") || undefined;
|
||||
const next_path = searchParams.get("next_path");
|
||||
// states
|
||||
const [authMode, setAuthMode] = useState<EAuthModes>(EAuthModes.SIGN_UP);
|
||||
const [authStep, setAuthStep] = useState<EAuthSteps>(EAuthSteps.EMAIL);
|
||||
const [email, setEmail] = useState(emailParam ? emailParam.toString() : "");
|
||||
const [errorInfo, setErrorInfo] = useState<TAuthErrorInfo | undefined>(undefined);
|
||||
const [isPasswordAutoset, setIsPasswordAutoset] = useState(true);
|
||||
// hooks
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { config } = useInstance();
|
||||
|
||||
useEffect(() => {
|
||||
if (error_code) {
|
||||
const errorhandler = authErrorHandler(error_code?.toString() as EAuthenticationErrorCodes);
|
||||
if (errorhandler) {
|
||||
if (errorhandler.code === EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_IN) {
|
||||
setAuthMode(EAuthModes.SIGN_IN);
|
||||
setAuthStep(EAuthSteps.PASSWORD);
|
||||
}
|
||||
if (errorhandler.code === EAuthenticationErrorCodes.AUTHENTICATION_FAILED_SIGN_UP) {
|
||||
setAuthMode(EAuthModes.SIGN_UP);
|
||||
setAuthStep(EAuthSteps.PASSWORD);
|
||||
}
|
||||
if (
|
||||
[
|
||||
EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_IN,
|
||||
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_IN,
|
||||
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_IN,
|
||||
].includes(errorhandler.code)
|
||||
) {
|
||||
setAuthMode(EAuthModes.SIGN_IN);
|
||||
setAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||
}
|
||||
if (
|
||||
[
|
||||
EAuthenticationErrorCodes.INVALID_MAGIC_CODE_SIGN_UP,
|
||||
EAuthenticationErrorCodes.EXPIRED_MAGIC_CODE_SIGN_UP,
|
||||
EAuthenticationErrorCodes.EMAIL_CODE_ATTEMPT_EXHAUSTED_SIGN_UP,
|
||||
].includes(errorhandler.code)
|
||||
) {
|
||||
setAuthMode(EAuthModes.SIGN_UP);
|
||||
setAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||
}
|
||||
setErrorInfo(errorhandler);
|
||||
}
|
||||
}
|
||||
}, [error_code]);
|
||||
|
||||
const isSMTPConfigured = config?.is_smtp_configured || false;
|
||||
const isMagicLoginEnabled = config?.is_magic_login_enabled || false;
|
||||
const isEmailPasswordEnabled = config?.is_email_password_enabled || false;
|
||||
const isOAuthEnabled =
|
||||
(config &&
|
||||
(config?.is_google_enabled ||
|
||||
config?.is_github_enabled ||
|
||||
config?.is_gitlab_enabled ||
|
||||
config?.is_gitea_enabled)) ||
|
||||
false;
|
||||
|
||||
// submit handler- email verification
|
||||
const handleEmailVerification = async (data: IEmailCheckData) => {
|
||||
setEmail(data.email);
|
||||
|
||||
await authService
|
||||
.emailCheck(data)
|
||||
.then(async (response) => {
|
||||
let currentAuthMode: EAuthModes = response.existing ? EAuthModes.SIGN_IN : EAuthModes.SIGN_UP;
|
||||
if (response.existing) {
|
||||
currentAuthMode = EAuthModes.SIGN_IN;
|
||||
setAuthMode(() => EAuthModes.SIGN_IN);
|
||||
} else {
|
||||
currentAuthMode = EAuthModes.SIGN_UP;
|
||||
setAuthMode(() => EAuthModes.SIGN_UP);
|
||||
}
|
||||
|
||||
if (currentAuthMode === EAuthModes.SIGN_IN) {
|
||||
if (response.is_password_autoset && isSMTPConfigured && isMagicLoginEnabled) {
|
||||
setAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||
generateEmailUniqueCode(data.email);
|
||||
} else if (isEmailPasswordEnabled) {
|
||||
setIsPasswordAutoset(false);
|
||||
setAuthStep(EAuthSteps.PASSWORD);
|
||||
} else {
|
||||
const errorhandler = authErrorHandler("5005" as EAuthenticationErrorCodes);
|
||||
setErrorInfo(errorhandler);
|
||||
}
|
||||
} else {
|
||||
if (isSMTPConfigured && isMagicLoginEnabled) {
|
||||
setAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||
generateEmailUniqueCode(data.email);
|
||||
} else if (isEmailPasswordEnabled) {
|
||||
setAuthStep(EAuthSteps.PASSWORD);
|
||||
} else {
|
||||
const errorhandler = authErrorHandler("5006" as EAuthenticationErrorCodes);
|
||||
setErrorInfo(errorhandler);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const errorhandler = authErrorHandler(error?.error_code?.toString(), data?.email || undefined);
|
||||
if (errorhandler?.type) setErrorInfo(errorhandler);
|
||||
});
|
||||
};
|
||||
|
||||
// generating the unique code
|
||||
const generateEmailUniqueCode = async (email: string): Promise<{ code: string } | undefined> => {
|
||||
const payload = { email: email };
|
||||
return await authService
|
||||
.generateUniqueCode(payload)
|
||||
.then(() => ({ code: "" }))
|
||||
.catch((error) => {
|
||||
const errorhandler = authErrorHandler(error?.error_code.toString());
|
||||
if (errorhandler?.type) setErrorInfo(errorhandler);
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
const content = authMode === EAuthModes.SIGN_UP ? "Sign up" : "Sign in";
|
||||
|
||||
const OAuthConfig = [
|
||||
{
|
||||
id: "google",
|
||||
text: `${content} with Google`,
|
||||
icon: <Image src={GoogleLogo} height={18} width={18} alt="Google Logo" />,
|
||||
onClick: () => {
|
||||
window.location.assign(`${API_BASE_URL}/auth/google/${next_path ? `?next_path=${next_path}` : ``}`);
|
||||
},
|
||||
enabled: config?.is_google_enabled,
|
||||
},
|
||||
{
|
||||
id: "github",
|
||||
text: `${content} with GitHub`,
|
||||
icon: (
|
||||
<Image
|
||||
src={resolvedTheme === "dark" ? GithubLightLogo : GithubDarkLogo}
|
||||
height={18}
|
||||
width={18}
|
||||
alt="GitHub Logo"
|
||||
/>
|
||||
),
|
||||
onClick: () => {
|
||||
window.location.assign(`${API_BASE_URL}/auth/github/${next_path ? `?next_path=${next_path}` : ``}`);
|
||||
},
|
||||
enabled: config?.is_github_enabled,
|
||||
},
|
||||
{
|
||||
id: "gitlab",
|
||||
text: `${content} with GitLab`,
|
||||
icon: <Image src={GitlabLogo} height={18} width={18} alt="GitLab Logo" />,
|
||||
onClick: () => {
|
||||
window.location.assign(`${API_BASE_URL}/auth/gitlab/${next_path ? `?next_path=${next_path}` : ``}`);
|
||||
},
|
||||
enabled: config?.is_gitlab_enabled,
|
||||
},
|
||||
{
|
||||
id: "gitea",
|
||||
text: `${content} with Gitea`,
|
||||
icon: <Image src={GiteaLogo} height={18} width={18} alt="Gitea Logo" />,
|
||||
onClick: () => {
|
||||
window.location.assign(`${API_BASE_URL}/auth/gitea/${next_path ? `?next_path=${next_path}` : ``}`);
|
||||
},
|
||||
enabled: config?.is_gitea_enabled,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
|
||||
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
|
||||
{errorInfo && errorInfo?.type === EErrorAlertType.BANNER_ALERT && (
|
||||
<AuthBanner bannerData={errorInfo} handleBannerData={(value) => setErrorInfo(value)} />
|
||||
)}
|
||||
<AuthHeader authMode={authMode} />
|
||||
{isOAuthEnabled && <OAuthOptions options={OAuthConfig} compact={authStep === EAuthSteps.PASSWORD} />}
|
||||
|
||||
{authStep === EAuthSteps.EMAIL && <AuthEmailForm defaultEmail={email} onSubmit={handleEmailVerification} />}
|
||||
{authStep === EAuthSteps.UNIQUE_CODE && (
|
||||
<AuthUniqueCodeForm
|
||||
mode={authMode}
|
||||
email={email}
|
||||
nextPath={nextPath}
|
||||
handleEmailClear={() => {
|
||||
setEmail("");
|
||||
setAuthStep(EAuthSteps.EMAIL);
|
||||
}}
|
||||
generateEmailUniqueCode={generateEmailUniqueCode}
|
||||
/>
|
||||
)}
|
||||
{authStep === EAuthSteps.PASSWORD && (
|
||||
<AuthPasswordForm
|
||||
mode={authMode}
|
||||
isPasswordAutoset={isPasswordAutoset}
|
||||
isSMTPConfigured={isSMTPConfigured}
|
||||
email={email}
|
||||
nextPath={nextPath}
|
||||
handleEmailClear={() => {
|
||||
setEmail("");
|
||||
setAuthStep(EAuthSteps.EMAIL);
|
||||
}}
|
||||
handleAuthStep={(step: EAuthSteps) => {
|
||||
if (step === EAuthSteps.UNIQUE_CODE) generateEmailUniqueCode(email);
|
||||
setAuthStep(step);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<TermsAndConditions isSignUp={authMode === EAuthModes.SIGN_UP ? true : false} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
104
apps/space/core/components/account/auth-forms/email.tsx
Normal file
104
apps/space/core/components/account/auth-forms/email.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import type { FormEvent } from "react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// icons
|
||||
import { CircleAlert, XCircle } from "lucide-react";
|
||||
// types
|
||||
import { Button } from "@plane/propel/button";
|
||||
import type { IEmailCheckData } from "@plane/types";
|
||||
// ui
|
||||
import { Input, Spinner } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
import { checkEmailValidity } from "@/helpers/string.helper";
|
||||
|
||||
type TAuthEmailForm = {
|
||||
defaultEmail: string;
|
||||
onSubmit: (data: IEmailCheckData) => Promise<void>;
|
||||
};
|
||||
|
||||
export const AuthEmailForm: React.FC<TAuthEmailForm> = observer((props) => {
|
||||
const { onSubmit, defaultEmail } = props;
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [email, setEmail] = useState(defaultEmail);
|
||||
|
||||
const emailError = useMemo(
|
||||
() => (email && !checkEmailValidity(email) ? { email: "Email is invalid" } : undefined),
|
||||
[email]
|
||||
);
|
||||
|
||||
const handleFormSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
const payload: IEmailCheckData = {
|
||||
email: email,
|
||||
};
|
||||
await onSubmit(payload);
|
||||
setIsSubmitting(false);
|
||||
};
|
||||
|
||||
const isButtonDisabled = email.length === 0 || Boolean(emailError?.email) || isSubmitting;
|
||||
|
||||
const [isFocused, setIsFocused] = useState(true);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleFormSubmit} className="mt-5 space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<div
|
||||
className={cn(
|
||||
`relative flex items-center rounded-md bg-custom-background-100 border`,
|
||||
!isFocused && Boolean(emailError?.email) ? `border-red-500` : `border-custom-border-100`
|
||||
)}
|
||||
onFocus={() => {
|
||||
setIsFocused(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFocused(false);
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="name@company.com"
|
||||
className={`disable-autofill-style h-10 w-full placeholder:text-custom-text-400 autofill:bg-red-500 border-0 focus:bg-none active:bg-transparent`}
|
||||
autoComplete="on"
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
/>
|
||||
{email.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Clear email"
|
||||
onClick={() => {
|
||||
setEmail("");
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<XCircle className="h-10 w-11 px-3 stroke-custom-text-400 hover:cursor-pointer text-xs" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{emailError?.email && !isFocused && (
|
||||
<p className="flex items-center gap-1 text-xs text-red-600 px-0.5">
|
||||
<CircleAlert height={12} width={12} />
|
||||
{emailError.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
1
apps/space/core/components/account/auth-forms/index.ts
Normal file
1
apps/space/core/components/account/auth-forms/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./auth-root";
|
||||
244
apps/space/core/components/account/auth-forms/password.tsx
Normal file
244
apps/space/core/components/account/auth-forms/password.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Eye, EyeOff, XCircle } from "lucide-react";
|
||||
// plane imports
|
||||
import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Input, Spinner, PasswordStrengthIndicator } from "@plane/ui";
|
||||
import { getPasswordStrength } from "@plane/utils";
|
||||
// types
|
||||
import { EAuthModes, EAuthSteps } from "@/types/auth";
|
||||
|
||||
type Props = {
|
||||
email: string;
|
||||
isPasswordAutoset: boolean;
|
||||
isSMTPConfigured: boolean;
|
||||
mode: EAuthModes;
|
||||
nextPath: string | undefined;
|
||||
handleEmailClear: () => void;
|
||||
handleAuthStep: (step: EAuthSteps) => void;
|
||||
};
|
||||
|
||||
type TPasswordFormValues = {
|
||||
email: string;
|
||||
password: string;
|
||||
confirm_password?: string;
|
||||
};
|
||||
|
||||
const defaultValues: TPasswordFormValues = {
|
||||
email: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
const authService = new AuthService();
|
||||
|
||||
export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
|
||||
const { email, nextPath, isSMTPConfigured, handleAuthStep, handleEmailClear, mode } = props;
|
||||
// ref
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
// states
|
||||
const [csrfPromise, setCsrfPromise] = useState<Promise<{ csrf_token: string }> | undefined>(undefined);
|
||||
const [passwordFormData, setPasswordFormData] = useState<TPasswordFormValues>({ ...defaultValues, email });
|
||||
const [showPassword, setShowPassword] = useState({
|
||||
password: false,
|
||||
retypePassword: false,
|
||||
});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
|
||||
const [isRetryPasswordInputFocused, setIsRetryPasswordInputFocused] = useState(false);
|
||||
|
||||
const handleShowPassword = (key: keyof typeof showPassword) =>
|
||||
setShowPassword((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||
|
||||
const handleFormChange = (key: keyof TPasswordFormValues, value: string) =>
|
||||
setPasswordFormData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
useEffect(() => {
|
||||
if (csrfPromise === undefined) {
|
||||
const promise = authService.requestCSRFToken();
|
||||
setCsrfPromise(promise);
|
||||
}
|
||||
}, [csrfPromise]);
|
||||
|
||||
const redirectToUniqueCodeSignIn = async () => {
|
||||
handleAuthStep(EAuthSteps.UNIQUE_CODE);
|
||||
};
|
||||
|
||||
const passwordSupport = passwordFormData.password.length > 0 &&
|
||||
mode === EAuthModes.SIGN_UP &&
|
||||
getPasswordStrength(passwordFormData.password) != E_PASSWORD_STRENGTH.STRENGTH_VALID && (
|
||||
<PasswordStrengthIndicator password={passwordFormData.password} isFocused={isPasswordInputFocused} />
|
||||
);
|
||||
|
||||
const isButtonDisabled = useMemo(
|
||||
() =>
|
||||
!isSubmitting &&
|
||||
!!passwordFormData.password &&
|
||||
(mode === EAuthModes.SIGN_UP
|
||||
? getPasswordStrength(passwordFormData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID &&
|
||||
passwordFormData.password === passwordFormData.confirm_password
|
||||
: true)
|
||||
? false
|
||||
: true,
|
||||
[isSubmitting, mode, passwordFormData.confirm_password, passwordFormData.password]
|
||||
);
|
||||
|
||||
const password = passwordFormData.password ?? "";
|
||||
const confirmPassword = passwordFormData.confirm_password ?? "";
|
||||
const renderPasswordMatchError = !isRetryPasswordInputFocused || confirmPassword.length >= password.length;
|
||||
|
||||
const handleCSRFToken = async () => {
|
||||
if (!formRef || !formRef.current) return;
|
||||
const token = await csrfPromise;
|
||||
if (!token?.csrf_token) return;
|
||||
const csrfElement = formRef.current.querySelector("input[name=csrfmiddlewaretoken]");
|
||||
csrfElement?.setAttribute("value", token?.csrf_token);
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
ref={formRef}
|
||||
className="mt-5 space-y-4"
|
||||
method="POST"
|
||||
action={`${API_BASE_URL}/auth/spaces/${mode === EAuthModes.SIGN_IN ? "sign-in" : "sign-up"}/`}
|
||||
onSubmit={async (event) => {
|
||||
event.preventDefault();
|
||||
await handleCSRFToken();
|
||||
if (formRef.current) {
|
||||
formRef.current.submit();
|
||||
}
|
||||
setIsSubmitting(true);
|
||||
}}
|
||||
onError={() => setIsSubmitting(false)}
|
||||
>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" />
|
||||
<input type="hidden" value={passwordFormData.email} name="email" />
|
||||
<input type="hidden" value={nextPath} name="next_path" />
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-custom-text-300" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<div
|
||||
className={`relative flex items-center rounded-md bg-custom-background-100 border border-custom-border-100`}
|
||||
>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={passwordFormData.email}
|
||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||
placeholder="name@company.com"
|
||||
className={`disable-autofill-style h-10 w-full placeholder:text-custom-text-400 border-0`}
|
||||
disabled
|
||||
/>
|
||||
{passwordFormData.email.length > 0 && (
|
||||
<XCircle
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={handleEmailClear}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
|
||||
{mode === EAuthModes.SIGN_IN ? "Password" : "Set a password"}
|
||||
</label>
|
||||
<div className="relative flex items-center rounded-md bg-custom-background-100">
|
||||
<Input
|
||||
type={showPassword?.password ? "text" : "password"}
|
||||
name="password"
|
||||
value={passwordFormData.password}
|
||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||
placeholder="Enter password"
|
||||
className="disable-autofill-style h-10 w-full border border-custom-border-100 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
|
||||
onFocus={() => setIsPasswordInputFocused(true)}
|
||||
onBlur={() => setIsPasswordInputFocused(false)}
|
||||
autoComplete="on"
|
||||
autoFocus
|
||||
/>
|
||||
{showPassword?.password ? (
|
||||
<EyeOff
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => handleShowPassword("password")}
|
||||
/>
|
||||
) : (
|
||||
<Eye
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => handleShowPassword("password")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{passwordSupport}
|
||||
</div>
|
||||
|
||||
{mode === EAuthModes.SIGN_UP && (
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="confirm_password">
|
||||
Confirm password
|
||||
</label>
|
||||
<div className="relative flex items-center rounded-md bg-custom-background-100">
|
||||
<Input
|
||||
type={showPassword?.retypePassword ? "text" : "password"}
|
||||
name="confirm_password"
|
||||
value={passwordFormData.confirm_password}
|
||||
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
|
||||
placeholder="Confirm password"
|
||||
className="disable-autofill-style h-10 w-full border border-custom-border-100 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
|
||||
onFocus={() => setIsRetryPasswordInputFocused(true)}
|
||||
onBlur={() => setIsRetryPasswordInputFocused(false)}
|
||||
/>
|
||||
{showPassword?.retypePassword ? (
|
||||
<EyeOff
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => handleShowPassword("retypePassword")}
|
||||
/>
|
||||
) : (
|
||||
<Eye
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={() => handleShowPassword("retypePassword")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!!passwordFormData.confirm_password &&
|
||||
passwordFormData.password !== passwordFormData.confirm_password &&
|
||||
renderPasswordMatchError && <span className="text-sm text-red-500">Passwords don{"'"}t match</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2.5">
|
||||
{mode === EAuthModes.SIGN_IN ? (
|
||||
<>
|
||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||
{isSubmitting ? (
|
||||
<Spinner height="20px" width="20px" />
|
||||
) : isSMTPConfigured ? (
|
||||
"Continue"
|
||||
) : (
|
||||
"Go to workspace"
|
||||
)}
|
||||
</Button>
|
||||
{isSMTPConfigured && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={redirectToUniqueCodeSignIn}
|
||||
variant="outline-primary"
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
Sign in with unique code
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Create account"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
});
|
||||
153
apps/space/core/components/account/auth-forms/unique-code.tsx
Normal file
153
apps/space/core/components/account/auth-forms/unique-code.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { CircleCheck, XCircle } from "lucide-react";
|
||||
// plane imports
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { AuthService } from "@plane/services";
|
||||
import { Input, Spinner } from "@plane/ui";
|
||||
// hooks
|
||||
import useTimer from "@/hooks/use-timer";
|
||||
// types
|
||||
import { EAuthModes } from "@/types/auth";
|
||||
|
||||
// services
|
||||
const authService = new AuthService();
|
||||
|
||||
type TAuthUniqueCodeForm = {
|
||||
mode: EAuthModes;
|
||||
email: string;
|
||||
nextPath: string | undefined;
|
||||
handleEmailClear: () => void;
|
||||
generateEmailUniqueCode: (email: string) => Promise<{ code: string } | undefined>;
|
||||
};
|
||||
|
||||
type TUniqueCodeFormValues = {
|
||||
email: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
const defaultValues: TUniqueCodeFormValues = {
|
||||
email: "",
|
||||
code: "",
|
||||
};
|
||||
|
||||
export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
|
||||
const { mode, email, nextPath, handleEmailClear, generateEmailUniqueCode } = props;
|
||||
// derived values
|
||||
const defaultResetTimerValue = 5;
|
||||
// states
|
||||
const [uniqueCodeFormData, setUniqueCodeFormData] = useState<TUniqueCodeFormValues>({ ...defaultValues, email });
|
||||
const [isRequestingNewCode, setIsRequestingNewCode] = useState(false);
|
||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
// timer
|
||||
const { timer: resendTimerCode, setTimer: setResendCodeTimer } = useTimer(0);
|
||||
|
||||
const handleFormChange = (key: keyof TUniqueCodeFormValues, value: string) =>
|
||||
setUniqueCodeFormData((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
const generateNewCode = async (email: string) => {
|
||||
try {
|
||||
setIsRequestingNewCode(true);
|
||||
const uniqueCode = await generateEmailUniqueCode(email);
|
||||
setResendCodeTimer(defaultResetTimerValue);
|
||||
handleFormChange("code", uniqueCode?.code || "");
|
||||
setIsRequestingNewCode(false);
|
||||
} catch {
|
||||
setResendCodeTimer(0);
|
||||
console.error("Error while requesting new code");
|
||||
setIsRequestingNewCode(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (csrfToken === undefined)
|
||||
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||
}, [csrfToken]);
|
||||
|
||||
const isRequestNewCodeDisabled = isRequestingNewCode || resendTimerCode > 0;
|
||||
const isButtonDisabled = isRequestingNewCode || !uniqueCodeFormData.code || isSubmitting;
|
||||
|
||||
return (
|
||||
<form
|
||||
className="mt-5 space-y-4"
|
||||
method="POST"
|
||||
action={`${API_BASE_URL}/auth/spaces/${mode === EAuthModes.SIGN_IN ? "magic-sign-in" : "magic-sign-up"}/`}
|
||||
onSubmit={() => setIsSubmitting(true)}
|
||||
onError={() => setIsSubmitting(false)}
|
||||
>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<input type="hidden" value={uniqueCodeFormData.email} name="email" />
|
||||
<input type="hidden" value={nextPath} name="next_path" />
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-custom-text-300" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<div
|
||||
className={`relative flex items-center rounded-md bg-custom-background-100 border border-custom-border-100`}
|
||||
>
|
||||
<Input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={uniqueCodeFormData.email}
|
||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||
placeholder="name@company.com"
|
||||
className={`disable-autofill-style h-10 w-full placeholder:text-custom-text-400 border-0`}
|
||||
disabled
|
||||
/>
|
||||
{uniqueCodeFormData.email.length > 0 && (
|
||||
<XCircle
|
||||
className="absolute right-3 h-5 w-5 stroke-custom-text-400 hover:cursor-pointer"
|
||||
onClick={handleEmailClear}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm font-medium text-custom-text-300" htmlFor="code">
|
||||
Unique code
|
||||
</label>
|
||||
<Input
|
||||
name="code"
|
||||
value={uniqueCodeFormData.code}
|
||||
onChange={(e) => handleFormChange("code", e.target.value)}
|
||||
placeholder="gets-sets-flys"
|
||||
className="disable-autofill-style h-10 w-full border border-custom-border-100 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex w-full items-center justify-between px-1 text-xs pt-1">
|
||||
<p className="flex items-center gap-1 font-medium text-green-700">
|
||||
<CircleCheck height={12} width={12} />
|
||||
Paste the code sent to your email
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => generateNewCode(uniqueCodeFormData.email)}
|
||||
className={`${
|
||||
isRequestNewCodeDisabled
|
||||
? "text-custom-text-400"
|
||||
: "font-medium text-custom-primary-300 hover:text-custom-primary-200"
|
||||
}`}
|
||||
disabled={isRequestNewCodeDisabled}
|
||||
>
|
||||
{resendTimerCode > 0
|
||||
? `Resend in ${resendTimerCode}s`
|
||||
: isRequestingNewCode
|
||||
? "Requesting new code"
|
||||
: "Resend"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<Button type="submit" variant="primary" className="w-full" size="lg" disabled={isButtonDisabled}>
|
||||
{isRequestingNewCode ? "Sending code" : isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
26
apps/space/core/components/account/terms-and-conditions.tsx
Normal file
26
apps/space/core/components/account/terms-and-conditions.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
type Props = {
|
||||
isSignUp?: boolean;
|
||||
};
|
||||
|
||||
export const TermsAndConditions: React.FC<Props> = (props) => {
|
||||
const { isSignUp = false } = props;
|
||||
return (
|
||||
<span className="flex items-center justify-center py-6">
|
||||
<p className="text-center text-sm text-custom-text-200 whitespace-pre-line">
|
||||
{isSignUp ? "By creating an account" : "By signing in"}, you agree to our{" \n"}
|
||||
<Link href="https://plane.so/legals/terms-and-conditions" target="_blank" rel="noopener noreferrer">
|
||||
<span className="text-sm font-medium underline hover:cursor-pointer">Terms of Service</span>
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link href="https://plane.so/legals/privacy-policy" target="_blank" rel="noopener noreferrer">
|
||||
<span className="text-sm font-medium underline hover:cursor-pointer">Privacy Policy</span>
|
||||
</Link>
|
||||
{"."}
|
||||
</p>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
44
apps/space/core/components/account/user-logged-in.tsx
Normal file
44
apps/space/core/components/account/user-logged-in.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { PlaneLockup } from "@plane/propel/icons";
|
||||
// assets
|
||||
import UserLoggedInImage from "@/app/assets/user-logged-in.svg?url";
|
||||
// components
|
||||
import { PoweredBy } from "@/components/common/powered-by";
|
||||
import { UserAvatar } from "@/components/issues/navbar/user-avatar";
|
||||
// hooks
|
||||
import { useUser } from "@/hooks/store/use-user";
|
||||
|
||||
export const UserLoggedIn = observer(() => {
|
||||
// store hooks
|
||||
const { data: user } = useUser();
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-screen w-screen">
|
||||
<div className="relative flex w-full items-center justify-between gap-4 border-b border-custom-border-200 px-6 py-5">
|
||||
<PlaneLockup className="h-6 w-auto text-custom-text-100" />
|
||||
<UserAvatar />
|
||||
</div>
|
||||
|
||||
<div className="size-full grid place-items-center p-6">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto size-32 md:size-52 grid place-items-center rounded-full bg-custom-background-80">
|
||||
<div className="size-16 md:size-32 grid place-items-center">
|
||||
<Image src={UserLoggedInImage} alt="User already logged in" />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mt-8 md:mt-12 text-xl md:text-3xl font-semibold">Nice! Just one more step.</h1>
|
||||
<p className="mt-2 md:mt-4 text-sm md:text-base">
|
||||
Enter the public-share URL or link of the view or Page you are trying to see in the browser{"'"}s address
|
||||
bar.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<PoweredBy />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user