web: split Form into files
This commit is contained in:
parent
2719f4bb2d
commit
58fbfad17a
@ -1,268 +0,0 @@
|
||||
/* eslint-disable react/jsx-curly-newline */
|
||||
/* eslint-disable sonarjs/no-small-switch */
|
||||
import TelegramIcon from '../public/assets/images/telegram.svg';
|
||||
import styles from './Form.module.scss';
|
||||
import { publicRuntimeConfig } from '@/config/runtime';
|
||||
import { FormStateContext } from '@/context/form-state';
|
||||
import { useSocket } from '@/hooks/socket';
|
||||
import type { TelegramUrlResponse } from '@/types/error';
|
||||
import type { LdapUser } from '@/types/user';
|
||||
import axios, { isAxiosError } from 'axios';
|
||||
import Image from 'next/image';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
const ERROR_INVALID_CREDENTIALS = 'Неверный логин или пароль';
|
||||
const ERROR_SERVER = 'Не удалось войти. Повторите попытку позже';
|
||||
|
||||
const { APP_BASE_PATH, TELEGRAM_BOT_URL } = publicRuntimeConfig;
|
||||
|
||||
type FormData = {
|
||||
readonly login: string;
|
||||
readonly password: string;
|
||||
};
|
||||
|
||||
type FormProps = {
|
||||
readonly onSubmit: (data: FormData) => void;
|
||||
};
|
||||
|
||||
function redirect() {
|
||||
const redirectUrl =
|
||||
(window.location.pathname.replace(APP_BASE_PATH, '') || '/') + (window.location.search || '');
|
||||
|
||||
window.location.replace(redirectUrl);
|
||||
}
|
||||
|
||||
function BaseForm({ children, onSubmit }: FormProps & PropsWithChildren) {
|
||||
const { handleSubmit, register } = useForm<FormData>();
|
||||
const {
|
||||
state: { error, step },
|
||||
} = useContext(FormStateContext);
|
||||
|
||||
return (
|
||||
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
|
||||
<input
|
||||
disabled={step !== 'login'}
|
||||
type="text"
|
||||
placeholder="Логин"
|
||||
required
|
||||
autoComplete="on"
|
||||
{...register('login', { required: true })}
|
||||
/>
|
||||
<input
|
||||
disabled={step !== 'login'}
|
||||
type="password"
|
||||
placeholder="Пароль"
|
||||
required
|
||||
autoComplete="on"
|
||||
{...register('password', { required: true })}
|
||||
/>
|
||||
{step === 'telegram-login' ? (
|
||||
<a target="_blank" className="info" href={TELEGRAM_BOT_URL} rel="noreferrer">
|
||||
Открыть чат с ботом
|
||||
</a>
|
||||
) : null}
|
||||
{error ? <span className="error">{error}</span> : null}
|
||||
{children}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export const Form = {
|
||||
Default() {
|
||||
const {
|
||||
dispatch,
|
||||
state: { step, user },
|
||||
} = useContext(FormStateContext);
|
||||
|
||||
function handleRefreshToken() {
|
||||
axios
|
||||
.get('/refresh-token')
|
||||
.then(() => redirect())
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_SERVER },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'login' && user) {
|
||||
return (
|
||||
<button
|
||||
className={styles['button-submit']}
|
||||
type="submit"
|
||||
onClick={() => handleRefreshToken()}
|
||||
>
|
||||
Продолжить как <b>{user?.displayName || user.username}</b>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function handleLogin(data: FormData) {
|
||||
return axios
|
||||
.post('/login', data)
|
||||
.then(() => redirect())
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_INVALID_CREDENTIALS },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseForm onSubmit={(data) => handleLogin(data)}>
|
||||
<button className={styles['button-submit']} type="submit">
|
||||
Войти
|
||||
</button>
|
||||
</BaseForm>
|
||||
);
|
||||
},
|
||||
|
||||
Telegram() {
|
||||
const {
|
||||
dispatch,
|
||||
state: { step, user },
|
||||
} = useContext(FormStateContext);
|
||||
|
||||
const { socket } = useSocket();
|
||||
|
||||
useEffect(() => {
|
||||
if (step === 'telegram-login') {
|
||||
socket.open();
|
||||
socket.on('connect', () => {});
|
||||
|
||||
socket.on('auth-allow', () => {
|
||||
socket.off('connect');
|
||||
axios
|
||||
.get('/login-confirm')
|
||||
.then(() => redirect())
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_SERVER },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
socket.off('connect');
|
||||
};
|
||||
}, [dispatch, socket, step]);
|
||||
|
||||
function handleLogin(data: FormData) {
|
||||
axios
|
||||
.post<LdapUser>('/login', data)
|
||||
.then((res) => {
|
||||
dispatch({
|
||||
payload: {
|
||||
step: 'telegram',
|
||||
user: res.data,
|
||||
},
|
||||
type: 'set-step',
|
||||
});
|
||||
})
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_INVALID_CREDENTIALS },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function handleTelegramLogin() {
|
||||
axios
|
||||
.post<LdapUser>('/login-telegram')
|
||||
.then(() => {
|
||||
dispatch({
|
||||
payload: {
|
||||
step: 'telegram-login',
|
||||
},
|
||||
type: 'set-step',
|
||||
});
|
||||
})
|
||||
.catch((error_) => {
|
||||
let error = ERROR_SERVER;
|
||||
|
||||
if (isAxiosError<TelegramUrlResponse>(error_) && error_.response?.data?.message) {
|
||||
error = error_.response?.data?.message;
|
||||
}
|
||||
|
||||
return dispatch({
|
||||
payload: { error },
|
||||
type: 'set-error',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
function handleRefreshToken() {
|
||||
axios
|
||||
.get('/refresh-token')
|
||||
.then(() => redirect())
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_SERVER },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'login' && user) {
|
||||
return (
|
||||
<button
|
||||
className={styles['button-submit']}
|
||||
type="submit"
|
||||
onClick={() => handleRefreshToken()}
|
||||
>
|
||||
Продолжить как <b>{user?.displayName || user.username}</b>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'telegram') {
|
||||
return (
|
||||
<BaseForm onSubmit={() => handleTelegramLogin()}>
|
||||
<button type="submit" className={styles['button-telegram']}>
|
||||
<Image
|
||||
className={styles['button-telegram-icon']}
|
||||
src={TelegramIcon}
|
||||
width={24}
|
||||
height={22}
|
||||
alt="Telegram icon"
|
||||
/>
|
||||
Войти как <b>{user?.displayName}</b>
|
||||
</button>
|
||||
</BaseForm>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'telegram-login') {
|
||||
return (
|
||||
<BaseForm onSubmit={() => {}}>
|
||||
<button disabled type="submit" className={styles['button-telegram']}>
|
||||
<Image
|
||||
className={styles['button-telegram-icon']}
|
||||
src={TelegramIcon}
|
||||
width={24}
|
||||
height={22}
|
||||
alt="Telegram icon"
|
||||
/>
|
||||
Ожидаем подтверждения...
|
||||
</button>
|
||||
</BaseForm>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseForm onSubmit={(data) => handleLogin(data)}>
|
||||
<button className={styles['button-submit']} type="submit">
|
||||
Войти
|
||||
</button>
|
||||
</BaseForm>
|
||||
);
|
||||
},
|
||||
};
|
||||
44
apps/web/components/Form/base-form.tsx
Normal file
44
apps/web/components/Form/base-form.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import styles from './Form.module.scss';
|
||||
import type { FormData, FormProps } from './types';
|
||||
import { publicRuntimeConfig } from '@/config/runtime';
|
||||
import { FormStateContext } from '@/context/form-state';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
const { TELEGRAM_BOT_URL } = publicRuntimeConfig;
|
||||
|
||||
export function BaseForm({ children, onSubmit }: FormProps & PropsWithChildren) {
|
||||
const { handleSubmit, register } = useForm<FormData>();
|
||||
const {
|
||||
state: { error, step },
|
||||
} = useContext(FormStateContext);
|
||||
|
||||
return (
|
||||
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
|
||||
<input
|
||||
disabled={step !== 'login'}
|
||||
type="text"
|
||||
placeholder="Логин"
|
||||
required
|
||||
autoComplete="on"
|
||||
{...register('login', { required: true })}
|
||||
/>
|
||||
<input
|
||||
disabled={step !== 'login'}
|
||||
type="password"
|
||||
placeholder="Пароль"
|
||||
required
|
||||
autoComplete="on"
|
||||
{...register('password', { required: true })}
|
||||
/>
|
||||
{step === 'telegram-login' ? (
|
||||
<a target="_blank" className="info" href={TELEGRAM_BOT_URL} rel="noreferrer">
|
||||
Открыть чат с ботом
|
||||
</a>
|
||||
) : null}
|
||||
{error ? <span className="error">{error}</span> : null}
|
||||
{children}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
59
apps/web/components/Form/default-form.tsx
Normal file
59
apps/web/components/Form/default-form.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { BaseForm } from './base-form';
|
||||
import { ERROR_INVALID_CREDENTIALS, ERROR_SERVER } from './errors';
|
||||
import styles from './Form.module.scss';
|
||||
import type { FormData } from './types';
|
||||
import { FormStateContext } from '@/context/form-state';
|
||||
import { redirect } from '@/utils/router';
|
||||
import axios from 'axios';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export function DefaultForm() {
|
||||
const {
|
||||
dispatch,
|
||||
state: { step, user },
|
||||
} = useContext(FormStateContext);
|
||||
|
||||
function handleRefreshToken() {
|
||||
axios
|
||||
.get('/refresh-token')
|
||||
.then(() => redirect())
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_SERVER },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'login' && user) {
|
||||
return (
|
||||
<button
|
||||
className={styles['button-submit']}
|
||||
type="submit"
|
||||
onClick={() => handleRefreshToken()}
|
||||
>
|
||||
Продолжить как <b>{user?.displayName || user.username}</b>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function handleLogin(data: FormData) {
|
||||
return axios
|
||||
.post('/login', data)
|
||||
.then(() => redirect())
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_INVALID_CREDENTIALS },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseForm onSubmit={(data) => handleLogin(data)}>
|
||||
<button className={styles['button-submit']} type="submit">
|
||||
Войти
|
||||
</button>
|
||||
</BaseForm>
|
||||
);
|
||||
}
|
||||
2
apps/web/components/Form/errors.ts
Normal file
2
apps/web/components/Form/errors.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const ERROR_INVALID_CREDENTIALS = 'Неверный логин или пароль';
|
||||
export const ERROR_SERVER = 'Не удалось войти. Повторите попытку позже';
|
||||
2
apps/web/components/Form/index.tsx
Normal file
2
apps/web/components/Form/index.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './default-form';
|
||||
export * from './telegram-form';
|
||||
157
apps/web/components/Form/telegram-form.tsx
Normal file
157
apps/web/components/Form/telegram-form.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import TelegramIcon from '../../public/assets/images/telegram.svg';
|
||||
import { BaseForm } from './base-form';
|
||||
import { ERROR_INVALID_CREDENTIALS, ERROR_SERVER } from './errors';
|
||||
import styles from './Form.module.scss';
|
||||
import type { FormData } from './types';
|
||||
import { FormStateContext } from '@/context/form-state';
|
||||
import { useSocket } from '@/hooks/socket';
|
||||
import type { TelegramUrlResponse } from '@/types/error';
|
||||
import type { LdapUser } from '@/types/user';
|
||||
import { redirect } from '@/utils/router';
|
||||
import axios, { isAxiosError } from 'axios';
|
||||
import Image from 'next/image';
|
||||
import { useContext, useEffect } from 'react';
|
||||
|
||||
export function TelegramForm() {
|
||||
const {
|
||||
dispatch,
|
||||
state: { step, user },
|
||||
} = useContext(FormStateContext);
|
||||
|
||||
const { socket } = useSocket();
|
||||
|
||||
useEffect(() => {
|
||||
if (step === 'telegram-login') {
|
||||
socket.open();
|
||||
socket.on('connect', () => {});
|
||||
|
||||
socket.on('auth-allow', () => {
|
||||
socket.off('connect');
|
||||
axios
|
||||
.get('/login-confirm')
|
||||
.then(() => redirect())
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_SERVER },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
socket.off('connect');
|
||||
};
|
||||
}, [dispatch, socket, step]);
|
||||
|
||||
function handleLogin(data: FormData) {
|
||||
axios
|
||||
.post<LdapUser>('/login', data)
|
||||
.then((res) => {
|
||||
dispatch({
|
||||
payload: {
|
||||
step: 'telegram',
|
||||
user: res.data,
|
||||
},
|
||||
type: 'set-step',
|
||||
});
|
||||
})
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_INVALID_CREDENTIALS },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function handleTelegramLogin() {
|
||||
axios
|
||||
.post<LdapUser>('/login-telegram')
|
||||
.then(() => {
|
||||
dispatch({
|
||||
payload: {
|
||||
step: 'telegram-login',
|
||||
},
|
||||
type: 'set-step',
|
||||
});
|
||||
})
|
||||
.catch((error_) => {
|
||||
let error = ERROR_SERVER;
|
||||
|
||||
if (isAxiosError<TelegramUrlResponse>(error_) && error_.response?.data?.message) {
|
||||
error = error_.response?.data?.message;
|
||||
}
|
||||
|
||||
return dispatch({
|
||||
payload: { error },
|
||||
type: 'set-error',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleRefreshToken() {
|
||||
axios
|
||||
.get('/refresh-token')
|
||||
.then(() => redirect())
|
||||
.catch(() =>
|
||||
dispatch({
|
||||
payload: { error: ERROR_SERVER },
|
||||
type: 'set-error',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'login' && user) {
|
||||
return (
|
||||
<button
|
||||
className={styles['button-submit']}
|
||||
type="submit"
|
||||
onClick={() => handleRefreshToken()}
|
||||
>
|
||||
Продолжить как <b>{user?.displayName || user.username}</b>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'telegram') {
|
||||
return (
|
||||
<BaseForm onSubmit={() => handleTelegramLogin()}>
|
||||
<button type="submit" className={styles['button-telegram']}>
|
||||
<Image
|
||||
className={styles['button-telegram-icon']}
|
||||
src={TelegramIcon}
|
||||
width={24}
|
||||
height={22}
|
||||
alt="Telegram icon"
|
||||
/>
|
||||
Войти как <b>{user?.displayName}</b>
|
||||
</button>
|
||||
</BaseForm>
|
||||
);
|
||||
}
|
||||
|
||||
if (step === 'telegram-login') {
|
||||
return (
|
||||
<BaseForm onSubmit={() => {}}>
|
||||
<button disabled type="submit" className={styles['button-telegram']}>
|
||||
<Image
|
||||
className={styles['button-telegram-icon']}
|
||||
src={TelegramIcon}
|
||||
width={24}
|
||||
height={22}
|
||||
alt="Telegram icon"
|
||||
/>
|
||||
Ожидаем подтверждения...
|
||||
</button>
|
||||
</BaseForm>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseForm onSubmit={(data) => handleLogin(data)}>
|
||||
<button className={styles['button-submit']} type="submit">
|
||||
Войти
|
||||
</button>
|
||||
</BaseForm>
|
||||
);
|
||||
}
|
||||
7
apps/web/components/Form/types.tsx
Normal file
7
apps/web/components/Form/types.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export type FormData = {
|
||||
readonly login: string;
|
||||
readonly password: string;
|
||||
};
|
||||
export type FormProps = {
|
||||
readonly onSubmit: (data: FormData) => void;
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { Form } from './Form';
|
||||
import * as Form from './Form';
|
||||
import styles from './Login.module.scss';
|
||||
import { Logo } from '@/elements';
|
||||
|
||||
@ -7,7 +7,7 @@ export function Login({ tfa }) {
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.login}>
|
||||
<Logo />
|
||||
{tfa ? <Form.Telegram /> : <Form.Default />}
|
||||
{tfa ? <Form.TelegramForm /> : <Form.DefaultForm />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
10
apps/web/utils/router.ts
Normal file
10
apps/web/utils/router.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { publicRuntimeConfig } from '@/config/runtime';
|
||||
|
||||
const { APP_BASE_PATH } = publicRuntimeConfig;
|
||||
|
||||
export function redirect() {
|
||||
const redirectUrl =
|
||||
(window.location.pathname.replace(APP_BASE_PATH, '') || '/') + (window.location.search || '');
|
||||
|
||||
window.location.replace(redirectUrl);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user