This commit is contained in:
commit
33453ecc55
3475 files changed
+850363
No files matched your search
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export * from './ui';
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
|
||||
export const requestResetPasswordWithEmailSchema = z.object({
|
||||
email: z.email(),
|
||||
});
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
|
||||
export const resetPasswordWithEmailSchema = z
|
||||
.object({
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string().min(8),
|
||||
code: z.string(),
|
||||
email: z.email(),
|
||||
})
|
||||
.refine(({ password, confirmPassword }) => password === confirmPassword, {
|
||||
message: 'must equal Password',
|
||||
path: ['confirmPassword'],
|
||||
})
|
||||
.refine(({ password }) => /[A-Za-z]/.test(password), {
|
||||
message: 'must contain at least one letter.',
|
||||
path: ['password'],
|
||||
})
|
||||
.refine(
|
||||
({ password }) => /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]/.test(password),
|
||||
{
|
||||
message: 'must contain at least one special character.',
|
||||
path: ['password'],
|
||||
},
|
||||
)
|
||||
.refine(({ password }) => !/(.)\1/.test(password), {
|
||||
message: 'must not contain consecutive identical characters',
|
||||
path: ['password'],
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { default as RequestResetPasswordWithEmail } from './request-reset-password-with-email.ui';
|
||||
export { default as ResetPasswordWithEmailForm } from './reset-password-with-email-form.ui';
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useRouter } from 'next/router';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { Button, toast } from '@ufb/react';
|
||||
|
||||
import { Path, TextInput, useOAIMutation } from '@/shared';
|
||||
|
||||
import { requestResetPasswordWithEmailSchema } from '../request-reset-password-with-email.schema';
|
||||
|
||||
type FormType = z.infer<typeof requestResetPasswordWithEmailSchema>;
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const RequestResetPasswordWithEmail: React.FC<IProps> = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { register, handleSubmit, formState } = useForm<FormType>({
|
||||
resolver: zodResolver(requestResetPasswordWithEmailSchema),
|
||||
});
|
||||
|
||||
const { mutate, isPending } = useOAIMutation({
|
||||
method: 'post',
|
||||
path: '/api/admin/users/password/reset/code',
|
||||
queryOptions: {
|
||||
async onSuccess() {
|
||||
await router.push(Path.SIGN_IN);
|
||||
toast.success(t('v2.toast.success'));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit((data) => mutate(data))}>
|
||||
<div className="mb-12 flex flex-col gap-4">
|
||||
<TextInput
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder={t('v2.placeholder.text')}
|
||||
error={formState.errors.email?.message}
|
||||
{...register('email')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
size="medium"
|
||||
type="submit"
|
||||
loading={isPending}
|
||||
disabled={!formState.isValid}
|
||||
>
|
||||
{t('v2.auth.reset-password.button.send-email')}
|
||||
</Button>
|
||||
<Button
|
||||
size="medium"
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={router.back}
|
||||
>
|
||||
{t('button.back')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestResetPasswordWithEmail;
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useRouter } from 'next/router';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { Button, toast } from '@ufb/react';
|
||||
|
||||
import { Path, TextInput, useOAIMutation } from '@/shared';
|
||||
|
||||
import { resetPasswordWithEmailSchema } from '../reset-password-with-email.schema';
|
||||
|
||||
type FormType = z.infer<typeof resetPasswordWithEmailSchema>;
|
||||
|
||||
interface IProps {
|
||||
code: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
const ResetPasswordWithEmailForm: React.FC<IProps> = ({ code, email }) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const { handleSubmit, register, formState } = useForm<FormType>({
|
||||
resolver: zodResolver(resetPasswordWithEmailSchema),
|
||||
defaultValues: { code, email },
|
||||
});
|
||||
|
||||
const { mutate, isPending } = useOAIMutation({
|
||||
method: 'post',
|
||||
path: '/api/admin/users/password/reset',
|
||||
queryOptions: {
|
||||
async onSuccess() {
|
||||
toast.success(t('v2.toast.success'));
|
||||
await router.push(Path.SIGN_IN);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = ({ password }: FormType) =>
|
||||
mutate({ code, email, password });
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="mb-10 flex flex-col gap-4">
|
||||
<TextInput
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder={t('v2.placeholder.text')}
|
||||
{...register('email')}
|
||||
disabled
|
||||
/>
|
||||
<TextInput
|
||||
type="password"
|
||||
label="Password"
|
||||
placeholder={t('v2.placeholder.text')}
|
||||
error={formState.errors.password?.message}
|
||||
{...register('password')}
|
||||
/>
|
||||
<TextInput
|
||||
type="password"
|
||||
label="Confirm Password"
|
||||
placeholder={t('v2.placeholder.text')}
|
||||
error={formState.errors.confirmPassword?.message}
|
||||
{...register('confirmPassword')}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isPending}
|
||||
disabled={!formState.isDirty}
|
||||
size="medium"
|
||||
>
|
||||
{t('v2.button.confirm')}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPasswordWithEmailForm;
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export * from './ui';
|
||||
export * from './lib';
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { useOAuthCallback } from './use-oauth-callback';
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
import { toast } from '@ufb/react';
|
||||
|
||||
import type { IFetchError } from '@/shared';
|
||||
import { useUserStore } from '@/entities/user';
|
||||
|
||||
export const useOAuthCallback = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { signInWithOAuth } = useUserStore();
|
||||
const [status, setStatus] = useState<'loading' | 'error'>('loading');
|
||||
const processedCodeRef = useRef<string | null>(null);
|
||||
|
||||
const code = router.query.code as string | undefined;
|
||||
const error = router.query.error as string | undefined;
|
||||
const errorDescription = router.query.error_description as string | undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
toast.error(
|
||||
errorDescription ?? 'An error occurred during OAuth2.0 login. Please contact the administrator.',
|
||||
);
|
||||
setStatus('error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Next.js renders once before the query string is available. Do not leave
|
||||
// the callback in an ambiguous state while the router is hydrating.
|
||||
if (!router.isReady || !code || processedCodeRef.current === code) return;
|
||||
|
||||
// OAuth authorization codes are single-use. React StrictMode can run
|
||||
// effects twice during local development, so exchange each code once.
|
||||
processedCodeRef.current = code;
|
||||
|
||||
void signInWithOAuth({ code }).catch((error) => {
|
||||
if (error instanceof AxiosError && error.response) {
|
||||
const message = error.response.data as IFetchError;
|
||||
toast.error(
|
||||
message.message ??
|
||||
message.axiosError?.error ??
|
||||
'An error occurred during OAuth2.0 login. Please contact the administrator.',
|
||||
);
|
||||
} else {
|
||||
toast.error(
|
||||
'An error occurred during OAuth2.0 login. Please contact the administrator.',
|
||||
);
|
||||
}
|
||||
setStatus('error');
|
||||
});
|
||||
}, [code, error, errorDescription, router.isReady, signInWithOAuth]);
|
||||
|
||||
return { status };
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { default as SignInWithOAuthButton } from './sign-in-with-oauth-button.ui';
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { Button } from '@ufb/react';
|
||||
|
||||
import { useOAIQuery } from '@/shared';
|
||||
import { useTenantStore } from '@/entities/tenant';
|
||||
|
||||
interface IProps {}
|
||||
|
||||
const OAUTH_CALLBACK_URL_STORAGE_KEY = 'ufb.oauth.callback-url';
|
||||
const OAUTH_FORCE_LOGIN_STORAGE_KEY = 'ufb.oauth.force-login';
|
||||
|
||||
const SignInWithOAuthButton: React.FC<IProps> = () => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { tenant } = useTenantStore();
|
||||
|
||||
const callback_url = (router.query.callback_url ?? '') as string;
|
||||
const force_login =
|
||||
typeof window !== 'undefined' ?
|
||||
(router.query.force_login ??
|
||||
window.sessionStorage.getItem(OAUTH_FORCE_LOGIN_STORAGE_KEY) ??
|
||||
'')
|
||||
: '';
|
||||
|
||||
const { data } = useOAIQuery({
|
||||
path: '/api/admin/auth/signIn/oauth/loginURL',
|
||||
queryOptions: { enabled: tenant?.useOAuth ?? false },
|
||||
variables: {
|
||||
callback_url,
|
||||
force_login: force_login ? 'true' : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!force_login || !data?.url || typeof window === 'undefined') return;
|
||||
|
||||
if (callback_url) {
|
||||
window.sessionStorage.setItem(
|
||||
OAUTH_CALLBACK_URL_STORAGE_KEY,
|
||||
callback_url,
|
||||
);
|
||||
} else {
|
||||
window.sessionStorage.removeItem(OAUTH_CALLBACK_URL_STORAGE_KEY);
|
||||
}
|
||||
|
||||
window.sessionStorage.setItem(OAUTH_FORCE_LOGIN_STORAGE_KEY, 'true');
|
||||
window.location.assign(data.url);
|
||||
}, [callback_url, data?.url, force_login]);
|
||||
|
||||
if (tenant?.oauthConfig?.loginButtonType === 'GOOGLE') {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="medium"
|
||||
disabled={!data?.url}
|
||||
onClick={() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (callback_url) {
|
||||
window.sessionStorage.setItem(
|
||||
OAUTH_CALLBACK_URL_STORAGE_KEY,
|
||||
callback_url,
|
||||
);
|
||||
} else {
|
||||
window.sessionStorage.removeItem(OAUTH_CALLBACK_URL_STORAGE_KEY);
|
||||
}
|
||||
if (force_login) {
|
||||
window.sessionStorage.setItem(
|
||||
OAUTH_FORCE_LOGIN_STORAGE_KEY,
|
||||
'true',
|
||||
);
|
||||
} else {
|
||||
window.sessionStorage.removeItem(OAUTH_FORCE_LOGIN_STORAGE_KEY);
|
||||
}
|
||||
window.location.assign(data?.url ?? '');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/assets/images/google.svg"
|
||||
alt="Google"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
Google {t('button.sign-in')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="medium"
|
||||
disabled={!data?.url}
|
||||
onClick={() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (callback_url) {
|
||||
window.sessionStorage.setItem(
|
||||
OAUTH_CALLBACK_URL_STORAGE_KEY,
|
||||
callback_url,
|
||||
);
|
||||
} else {
|
||||
window.sessionStorage.removeItem(OAUTH_CALLBACK_URL_STORAGE_KEY);
|
||||
}
|
||||
if (force_login) {
|
||||
window.sessionStorage.setItem(
|
||||
OAUTH_FORCE_LOGIN_STORAGE_KEY,
|
||||
'true',
|
||||
);
|
||||
} else {
|
||||
window.sessionStorage.removeItem(OAUTH_FORCE_LOGIN_STORAGE_KEY);
|
||||
}
|
||||
window.location.assign(data?.url ?? '');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tenant?.oauthConfig?.loginButtonName ??
|
||||
`OAuth 2.0 ${t('button.sign-in')}`}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignInWithOAuthButton;
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export * from './ui';
|
||||
export type { SignUpWithEmailType } from './sign-up-with-email.type';
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
|
||||
//
|
||||
export const signUpWithEmailSchema = z
|
||||
.object({
|
||||
email: z.email(),
|
||||
emailState: z.enum(['NOT_VERIFIED', 'VERIFING', 'EXPIRED', 'VERIFIED']),
|
||||
code: z.string().length(6),
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string().min(8),
|
||||
})
|
||||
.refine((schema) => schema.password === schema.confirmPassword, {
|
||||
message: 'must equal Password',
|
||||
path: ['confirmPassword'],
|
||||
})
|
||||
.refine(({ password }) => /[A-Za-z]/.test(password), {
|
||||
message: 'must contain at least one letter.',
|
||||
path: ['password'],
|
||||
})
|
||||
.refine(
|
||||
({ password }) => /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]/.test(password),
|
||||
{
|
||||
message: 'must contain at least one special character.',
|
||||
path: ['password'],
|
||||
},
|
||||
)
|
||||
.refine(({ password }) => !/(.)\1/.test(password), {
|
||||
message: 'must not contain consecutive identical characters',
|
||||
path: ['password'],
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { z } from 'zod';
|
||||
|
||||
import type { signUpWithEmailSchema } from './sign-up-with-email.schema';
|
||||
|
||||
export type SignUpWithEmailType = z.infer<typeof signUpWithEmailSchema>;
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { default as SignUpWithEmailForm } from './sign-up-with-email-form.ui';
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useInterval } from 'react-use';
|
||||
|
||||
import { Button } from '@ufb/react';
|
||||
|
||||
import { TextInput, useOAIMutation } from '@/shared';
|
||||
|
||||
import { signUpWithEmailSchema } from '../sign-up-with-email.schema';
|
||||
import type { SignUpWithEmailType } from '../sign-up-with-email.type';
|
||||
|
||||
interface IProps {
|
||||
onSubmit: (data: SignUpWithEmailType) => void;
|
||||
loading?: boolean;
|
||||
submitText: string;
|
||||
initialValues?: SignUpWithEmailType | null;
|
||||
skipEmailVerification?: boolean;
|
||||
}
|
||||
|
||||
const SignUpWithEmailForm: React.FC<IProps> = (props) => {
|
||||
const {
|
||||
onSubmit,
|
||||
loading,
|
||||
submitText,
|
||||
initialValues,
|
||||
skipEmailVerification,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
formState,
|
||||
getValues,
|
||||
setValue,
|
||||
watch,
|
||||
setError,
|
||||
clearErrors,
|
||||
} = useForm<SignUpWithEmailType>({
|
||||
resolver: zodResolver(signUpWithEmailSchema),
|
||||
defaultValues:
|
||||
initialValues ?
|
||||
{ ...initialValues }
|
||||
: {
|
||||
emailState: skipEmailVerification ? 'VERIFIED' : 'NOT_VERIFIED',
|
||||
code: skipEmailVerification ? '000000' : '',
|
||||
},
|
||||
});
|
||||
|
||||
const [expiredTime, setExpiredTime] = useState<string>();
|
||||
const [leftTime, setLeftTime] = useState('');
|
||||
|
||||
const { mutate: fetchCode, status: fetchCodeStatus } = useOAIMutation({
|
||||
method: 'post',
|
||||
path: '/api/admin/auth/email/code',
|
||||
queryOptions: {
|
||||
onSuccess(data) {
|
||||
setValue('emailState', 'VERIFING');
|
||||
setExpiredTime(data?.expiredAt);
|
||||
clearErrors('email');
|
||||
},
|
||||
onError(error) {
|
||||
const { message } = error;
|
||||
setError('email', { message });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: verifyCode, status: verifyCodeStatus } = useOAIMutation({
|
||||
method: 'post',
|
||||
path: '/api/admin/auth/email/code/verify',
|
||||
queryOptions: {
|
||||
onSuccess() {
|
||||
setValue('emailState', 'VERIFIED');
|
||||
clearErrors('code');
|
||||
},
|
||||
onError(error) {
|
||||
const { message } = error;
|
||||
setError('code', { message });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
const seconds = dayjs(expiredTime).diff(dayjs(), 'seconds');
|
||||
|
||||
if (seconds < 0) {
|
||||
setLeftTime(`00:00`);
|
||||
setValue('emailState', 'EXPIRED');
|
||||
} else {
|
||||
const m = Math.floor(seconds / 60)
|
||||
.toString()
|
||||
.padStart(2, '0');
|
||||
const s = Math.floor(seconds % 60)
|
||||
.toString()
|
||||
.padStart(2, '0');
|
||||
|
||||
setLeftTime(`${m}:${s}`);
|
||||
}
|
||||
},
|
||||
watch('emailState') === 'VERIFING' ? 1000 : null,
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit((data) => onSubmit(data))}>
|
||||
<div className="mb-12 flex flex-col gap-4">
|
||||
<div className="flex gap-2">
|
||||
<TextInput
|
||||
type="email"
|
||||
{...register('email')}
|
||||
label="Email"
|
||||
placeholder={t('v2.placeholder.text')}
|
||||
disabled={
|
||||
!skipEmailVerification && watch('emailState') !== 'NOT_VERIFIED'
|
||||
}
|
||||
error={formState.errors.email?.message}
|
||||
infoCaption={
|
||||
!skipEmailVerification && watch('emailState') !== 'NOT_VERIFIED' ?
|
||||
t('v2.text.request-email-auth')
|
||||
: undefined
|
||||
}
|
||||
rightButton={
|
||||
skipEmailVerification ? undefined : (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => fetchCode({ email: getValues('email') })}
|
||||
loading={fetchCodeStatus === 'pending'}
|
||||
disabled={watch('emailState') === 'VERIFIED'}
|
||||
className="min-w-[96px]"
|
||||
>
|
||||
{t('v2.auth.sign-up.button.request-auth-code')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!skipEmailVerification && watch('emailState') !== 'NOT_VERIFIED' && (
|
||||
<div className="flex gap-2">
|
||||
<TextInput
|
||||
type="text"
|
||||
label="Authentication code"
|
||||
{...register('code')}
|
||||
disabled={watch('emailState') === 'VERIFIED'}
|
||||
placeholder={t('v2.placeholder.text')}
|
||||
error={formState.errors.code?.message}
|
||||
right={
|
||||
(watch('emailState') === 'VERIFING' ||
|
||||
watch('emailState') === 'EXPIRED') && (
|
||||
<p className="font-16-regular">{leftTime}</p>
|
||||
)
|
||||
}
|
||||
rightButton={
|
||||
<Button
|
||||
loading={verifyCodeStatus === 'pending'}
|
||||
disabled={
|
||||
watch('emailState') === 'VERIFIED' ||
|
||||
watch('code').length !== 6
|
||||
}
|
||||
onClick={() => {
|
||||
const code = getValues('code');
|
||||
if (!code) return;
|
||||
verifyCode({ code, email: getValues('email') });
|
||||
}}
|
||||
className="min-w-[96px]"
|
||||
>
|
||||
{t('v2.auth.sign-up.button.verify-auth-code')}
|
||||
</Button>
|
||||
}
|
||||
successCaption={
|
||||
watch('emailState') === 'VERIFIED' ? 'Authenticated.' : (
|
||||
undefined
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<TextInput
|
||||
type="password"
|
||||
label="Password"
|
||||
placeholder={t('v2.placeholder.text')}
|
||||
error={formState.errors.password?.message}
|
||||
{...register('password')}
|
||||
/>
|
||||
<TextInput
|
||||
type="password"
|
||||
label="Confirm Password"
|
||||
placeholder={t('v2.placeholder.text')}
|
||||
error={formState.errors.confirmPassword?.message}
|
||||
{...register('confirmPassword')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Button
|
||||
size="medium"
|
||||
type="submit"
|
||||
loading={loading}
|
||||
disabled={!formState.isDirty || watch('emailState') !== 'VERIFIED'}
|
||||
>
|
||||
{submitText}
|
||||
</Button>
|
||||
<Button size="medium" variant="outline" onClick={router.back}>
|
||||
{t('button.back')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignUpWithEmailForm;
|
||||
Reference in new issue
Block a user