mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-05-04 00:56:40 +07:00
removed redux completely
This commit is contained in:
parent
26e6d8d41b
commit
a4bb07e9cf
323 changed files with 2464 additions and 3348 deletions
20
src/hooks/auth/useAuth.ts
Normal file
20
src/hooks/auth/useAuth.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { useUser } from '~/hooks/auth/useUser';
|
||||
import { useAuthStore } from '~/store/auth/useAuthStore';
|
||||
import { useLoginLogoutRestore } from '~/hooks/auth/useLoginLogoutRestore';
|
||||
|
||||
export const useAuth = () => {
|
||||
const { user } = useUser();
|
||||
const auth = useAuthStore();
|
||||
|
||||
const { login, logout } = useLoginLogoutRestore();
|
||||
|
||||
return {
|
||||
user,
|
||||
logout,
|
||||
login,
|
||||
isUser: auth.isUser,
|
||||
setToken: auth.setToken,
|
||||
isTester: auth.isTester,
|
||||
setIsTester: auth.setIsTester,
|
||||
};
|
||||
};
|
16
src/hooks/auth/useLastSeenBoris.ts
Normal file
16
src/hooks/auth/useLastSeenBoris.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useUser } from '~/hooks/auth/useUser';
|
||||
|
||||
export const useLastSeenBoris = () => {
|
||||
const { user, update } = useUser();
|
||||
const lastSeen = user.last_seen_boris;
|
||||
|
||||
const setLastSeen = useCallback(
|
||||
async (date: string) => {
|
||||
await update({ last_seen_boris: date }, false);
|
||||
},
|
||||
[update]
|
||||
);
|
||||
|
||||
return { setLastSeen, lastSeen };
|
||||
};
|
48
src/hooks/auth/useLoginForm.ts
Normal file
48
src/hooks/auth/useLoginForm.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { Asserts, object, string } from 'yup';
|
||||
import { ERRORS } from '~/constants/errors';
|
||||
import { useCallback } from 'react';
|
||||
import { FormikConfig, useFormik } from 'formik';
|
||||
import { IUser } from '~/types/auth';
|
||||
import { showToastSuccess } from '~/utils/toast';
|
||||
import { getRandomPhrase } from '~/constants/phrases';
|
||||
import { showErrorToast } from '~/utils/errors/showToast';
|
||||
import { getValidationErrors } from '~/utils/errors/getValidationErrors';
|
||||
|
||||
const validationSchema = object({
|
||||
username: string().required(ERRORS.REQUIRED),
|
||||
password: string().required(ERRORS.REQUIRED),
|
||||
});
|
||||
|
||||
export type LoginFormData = Asserts<typeof validationSchema>;
|
||||
|
||||
export const useLoginForm = (
|
||||
fetcher: (username: string, password: string) => Promise<IUser>,
|
||||
onSuccess: () => void
|
||||
) => {
|
||||
const onSubmit = useCallback<FormikConfig<LoginFormData>['onSubmit']>(
|
||||
async (values, { setErrors }) => {
|
||||
try {
|
||||
await fetcher(values.username, values.password);
|
||||
onSuccess();
|
||||
showToastSuccess(getRandomPhrase('WELCOME'));
|
||||
} catch (error) {
|
||||
showErrorToast(error);
|
||||
|
||||
const validationErrors = getValidationErrors(error);
|
||||
if (validationErrors) {
|
||||
setErrors(validationErrors);
|
||||
}
|
||||
}
|
||||
},
|
||||
[fetcher, onSuccess]
|
||||
);
|
||||
|
||||
return useFormik({
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
});
|
||||
};
|
20
src/hooks/auth/useLoginLogoutRestore.ts
Normal file
20
src/hooks/auth/useLoginLogoutRestore.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { useAuthStore } from '~/store/auth/useAuthStore';
|
||||
import { useCallback } from 'react';
|
||||
import { apiUserLogin } from '~/api/auth';
|
||||
|
||||
export const useLoginLogoutRestore = () => {
|
||||
const auth = useAuthStore();
|
||||
|
||||
const logout = useCallback(() => auth.logout(), [auth]);
|
||||
|
||||
const login = useCallback(
|
||||
async (username: string, password: string) => {
|
||||
const result = await apiUserLogin({ username, password });
|
||||
auth.setToken(result.token);
|
||||
return result.user;
|
||||
},
|
||||
[auth]
|
||||
);
|
||||
|
||||
return { logout, login };
|
||||
};
|
53
src/hooks/auth/useMessageEventReactions.ts
Normal file
53
src/hooks/auth/useMessageEventReactions.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { useEffect } from 'react';
|
||||
import { EventMessageType } from '~/constants/events';
|
||||
import { includes, path, values } from 'ramda';
|
||||
import { useOAuth } from '~/hooks/auth/useOAuth';
|
||||
import { Dialog } from '~/constants/modal';
|
||||
import { useModal } from '~/hooks/modal/useModal';
|
||||
import { useAuth } from '~/hooks/auth/useAuth';
|
||||
|
||||
/** reacts to events passed by window.postMessage */
|
||||
export const useMessageEventReactions = () => {
|
||||
const { loginWithSocial, createSocialAccount, attachAccount } = useOAuth();
|
||||
const { showModal } = useModal();
|
||||
const { isUser } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
const type: EventMessageType | undefined = path(['data', 'type'], event);
|
||||
|
||||
if (!type || !includes(type, values(EventMessageType))) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('caught event:', type);
|
||||
|
||||
switch (type) {
|
||||
case EventMessageType.OAuthLogin:
|
||||
// TODO: do we really need it?
|
||||
loginWithSocial(path(['data', 'payload', 'token'], event));
|
||||
break;
|
||||
case EventMessageType.OAuthProcessed:
|
||||
if (isUser) {
|
||||
void attachAccount(path(['data', 'payload', 'token'], event));
|
||||
} else {
|
||||
void createSocialAccount(path(['data', 'payload', 'token'], event));
|
||||
}
|
||||
break;
|
||||
case EventMessageType.OpenProfile:
|
||||
const username: string | undefined = path(['data', 'username'], event);
|
||||
if (!username) {
|
||||
return;
|
||||
}
|
||||
|
||||
showModal(Dialog.Profile, { username });
|
||||
break;
|
||||
default:
|
||||
console.log('unknown message', event.data);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', onMessage);
|
||||
return () => window.removeEventListener('message', onMessage);
|
||||
}, [attachAccount, createSocialAccount, loginWithSocial, isUser, showModal]);
|
||||
};
|
101
src/hooks/auth/useOAuth.ts
Normal file
101
src/hooks/auth/useOAuth.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import { useCallback } from 'react';
|
||||
import { OAuthProvider } from '~/types/auth';
|
||||
import { API } from '~/constants/api';
|
||||
import { apiAttachSocial, apiDropSocial, apiGetSocials, apiLoginWithSocial } from '~/api/auth';
|
||||
import { useModal } from '~/hooks/modal/useModal';
|
||||
import { Dialog } from '~/constants/modal';
|
||||
import { showErrorToast } from '~/utils/errors/showToast';
|
||||
import { path } from 'ramda';
|
||||
import useSWR from 'swr';
|
||||
import { useAuth } from '~/hooks/auth/useAuth';
|
||||
|
||||
export const useOAuth = () => {
|
||||
const { isUser, setToken } = useAuth();
|
||||
const { showModal, hideModal } = useModal();
|
||||
|
||||
const { data, isValidating: isLoading, mutate } = useSWR(
|
||||
isUser ? API.USER.GET_SOCIALS : null,
|
||||
async () => {
|
||||
const result = await apiGetSocials();
|
||||
return result.accounts;
|
||||
}
|
||||
);
|
||||
|
||||
const openOauthWindow = useCallback((provider: OAuthProvider) => {
|
||||
window.open(API.USER.OAUTH_WINDOW(provider), '', 'width=600,height=400');
|
||||
}, []);
|
||||
|
||||
const createSocialAccount = useCallback(
|
||||
async (token?: string) => {
|
||||
try {
|
||||
if (!token) return;
|
||||
|
||||
const result = await apiLoginWithSocial({ token });
|
||||
|
||||
setToken(result.token);
|
||||
hideModal();
|
||||
} catch (error) {
|
||||
const needsRegister: string | undefined = path(
|
||||
['response', 'data', 'needs_register'],
|
||||
error
|
||||
);
|
||||
|
||||
if (needsRegister && token) {
|
||||
showModal(Dialog.LoginSocialRegister, { token });
|
||||
return;
|
||||
}
|
||||
|
||||
showErrorToast(error);
|
||||
}
|
||||
},
|
||||
[showModal, hideModal, setToken]
|
||||
);
|
||||
|
||||
const loginWithSocial = useCallback(
|
||||
(token: string | undefined) => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
setToken(token);
|
||||
hideModal();
|
||||
},
|
||||
[setToken, hideModal]
|
||||
);
|
||||
|
||||
const attachAccount = useCallback(
|
||||
async (token?: string) => {
|
||||
try {
|
||||
if (!token) return;
|
||||
|
||||
await apiAttachSocial({ token });
|
||||
await mutate();
|
||||
} catch (error) {
|
||||
showErrorToast(error);
|
||||
}
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
|
||||
const dropAccount = useCallback(
|
||||
async (provider: OAuthProvider, id: string) => {
|
||||
try {
|
||||
await apiDropSocial({ id, provider });
|
||||
await mutate();
|
||||
} catch (error) {
|
||||
showErrorToast(error);
|
||||
}
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
|
||||
return {
|
||||
openOauthWindow,
|
||||
loginWithSocial,
|
||||
createSocialAccount,
|
||||
attachAccount,
|
||||
dropAccount,
|
||||
accounts: data || [],
|
||||
isLoading: !data && isLoading,
|
||||
};
|
||||
};
|
35
src/hooks/auth/usePatchUser.ts
Normal file
35
src/hooks/auth/usePatchUser.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { useUploader } from '~/hooks/data/useUploader';
|
||||
import { UploadSubject, UploadTarget } from '~/constants/uploads';
|
||||
import { useCallback } from 'react';
|
||||
import { showErrorToast } from '~/utils/errors/showToast';
|
||||
import { apiUpdateUser } from '~/api/auth';
|
||||
import { ApiUpdateUserRequest } from '~/api/auth/types';
|
||||
import { useUser } from '~/hooks/auth/useUser';
|
||||
|
||||
export const usePatchUser = () => {
|
||||
const { update } = useUser();
|
||||
const { uploadFile } = useUploader(UploadSubject.Avatar, UploadTarget.Profiles);
|
||||
|
||||
const save = useCallback(
|
||||
async (user: Partial<ApiUpdateUserRequest['user']>) => {
|
||||
const result = await apiUpdateUser({ user });
|
||||
await update(result.user);
|
||||
return result.user;
|
||||
},
|
||||
[update]
|
||||
);
|
||||
|
||||
const updatePhoto = useCallback(
|
||||
async (file: File) => {
|
||||
try {
|
||||
const photo = await uploadFile(file);
|
||||
await save({ photo });
|
||||
} catch (error) {
|
||||
showErrorToast(error);
|
||||
}
|
||||
},
|
||||
[uploadFile, save]
|
||||
);
|
||||
|
||||
return { updatePhoto, save };
|
||||
};
|
14
src/hooks/auth/useRestoreCode.ts
Normal file
14
src/hooks/auth/useRestoreCode.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { apiCheckRestoreCode } from '~/api/auth';
|
||||
import useSWR from 'swr';
|
||||
import { API } from '~/constants/api';
|
||||
import { getErrorMessage } from '~/utils/errors/getErrorMessage';
|
||||
|
||||
export const useRestoreCode = (code: string) => {
|
||||
const { data, isValidating, error } = useSWR(API.USER.REQUEST_CODE(code), () =>
|
||||
apiCheckRestoreCode({ code })
|
||||
);
|
||||
|
||||
const codeUser = data?.user;
|
||||
|
||||
return { codeUser, isLoading: isValidating, error: getErrorMessage(error) };
|
||||
};
|
56
src/hooks/auth/useRestorePasswordForm.ts
Normal file
56
src/hooks/auth/useRestorePasswordForm.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { Asserts, object, string } from 'yup';
|
||||
import { FormikConfig, useFormik } from 'formik';
|
||||
import { useCallback } from 'react';
|
||||
import { showErrorToast } from '~/utils/errors/showToast';
|
||||
import { getValidationErrors } from '~/utils/errors/getValidationErrors';
|
||||
import { ERRORS } from '~/constants/errors';
|
||||
import { IUser } from '~/types/auth';
|
||||
import { useAuthStore } from '~/store/auth/useAuthStore';
|
||||
|
||||
const validationSchema = object({
|
||||
newPassword: string()
|
||||
.required(ERRORS.REQUIRED)
|
||||
.min(6, ERRORS.PASSWORD_IS_SHORT),
|
||||
newPasswordAgain: string()
|
||||
.required(ERRORS.REQUIRED)
|
||||
.test(
|
||||
'sameAsPassword',
|
||||
'Должен совпадать с паролем',
|
||||
(val, ctx) => val === ctx.parent.newPassword
|
||||
),
|
||||
});
|
||||
|
||||
export type RestorePasswordData = Asserts<typeof validationSchema>;
|
||||
|
||||
export const useRestorePasswordForm = (
|
||||
code: string,
|
||||
fetcher: (props: { code: string; password: string }) => Promise<{ token: string; user: IUser }>,
|
||||
onSuccess: () => void
|
||||
) => {
|
||||
const auth = useAuthStore();
|
||||
|
||||
const onSubmit = useCallback<FormikConfig<RestorePasswordData>['onSubmit']>(
|
||||
async (values, { setErrors }) => {
|
||||
try {
|
||||
const { token, user } = await fetcher({ password: values.newPassword, code });
|
||||
auth.setUser(user);
|
||||
auth.setToken(token);
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
showErrorToast(error);
|
||||
|
||||
const validationErrors = getValidationErrors(error);
|
||||
if (validationErrors) {
|
||||
setErrors(validationErrors);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onSuccess, fetcher, code, auth]
|
||||
);
|
||||
|
||||
return useFormik<RestorePasswordData>({
|
||||
onSubmit,
|
||||
validationSchema,
|
||||
initialValues: { newPassword: '', newPasswordAgain: '' },
|
||||
});
|
||||
};
|
21
src/hooks/auth/useRestorePasswordRedirect.ts
Normal file
21
src/hooks/auth/useRestorePasswordRedirect.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { useModal } from '~/hooks/modal/useModal';
|
||||
import { Dialog } from '~/constants/modal';
|
||||
|
||||
/** redirects to the password redirect modal */
|
||||
export const useRestorePasswordRedirect = () => {
|
||||
const history = useHistory();
|
||||
const { showModal } = useModal();
|
||||
|
||||
useEffect(() => {
|
||||
const match = window.location.pathname.match(/^\/restore\/([\w\d-]+)$/);
|
||||
|
||||
if (!match?.[1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
history.push('/');
|
||||
showModal(Dialog.RestorePassword, { code: match[1] });
|
||||
}, [showModal, history]);
|
||||
};
|
40
src/hooks/auth/useRestoreRequestForm.ts
Normal file
40
src/hooks/auth/useRestoreRequestForm.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Asserts, object, string } from 'yup';
|
||||
import { ERRORS } from '~/constants/errors';
|
||||
import { FormikConfig, useFormik } from 'formik';
|
||||
import { useCallback } from 'react';
|
||||
import { showErrorToast } from '~/utils/errors/showToast';
|
||||
import { getValidationErrors } from '~/utils/errors/getValidationErrors';
|
||||
|
||||
const validationSchema = object({
|
||||
field: string().required(ERRORS.REQUIRED),
|
||||
});
|
||||
|
||||
type RestoreRequestData = Asserts<typeof validationSchema>;
|
||||
|
||||
export const useRestoreRequestForm = (
|
||||
fetcher: (field: string) => Promise<unknown>,
|
||||
onSuccess: () => void
|
||||
) => {
|
||||
const onSubmit = useCallback<FormikConfig<RestoreRequestData>['onSubmit']>(
|
||||
async (values, { setErrors }) => {
|
||||
try {
|
||||
await fetcher(values.field);
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
showErrorToast(error);
|
||||
|
||||
const validationErrors = getValidationErrors(error);
|
||||
if (validationErrors) {
|
||||
setErrors(validationErrors);
|
||||
}
|
||||
}
|
||||
},
|
||||
[fetcher, onSuccess]
|
||||
);
|
||||
|
||||
return useFormik({
|
||||
onSubmit,
|
||||
validationSchema,
|
||||
initialValues: { field: '' },
|
||||
});
|
||||
};
|
55
src/hooks/auth/useSocialRegisterForm.ts
Normal file
55
src/hooks/auth/useSocialRegisterForm.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { Asserts, object, string } from 'yup';
|
||||
import { ERRORS } from '~/constants/errors';
|
||||
import { FormikConfig, useFormik } from 'formik';
|
||||
import { useCallback } from 'react';
|
||||
import { showErrorToast } from '~/utils/errors/showToast';
|
||||
import { getValidationErrors } from '~/utils/errors/getValidationErrors';
|
||||
|
||||
const validationSchema = object({
|
||||
username: string().required(ERRORS.REQUIRED),
|
||||
password: string()
|
||||
.required(ERRORS.REQUIRED)
|
||||
.min(6, ERRORS.PASSWORD_IS_SHORT),
|
||||
});
|
||||
|
||||
type SocialRegisterData = Asserts<typeof validationSchema>;
|
||||
|
||||
export const useSocialRegisterForm = (
|
||||
token: string,
|
||||
fetcher: (props: {
|
||||
token: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}) => Promise<{ token: string }>,
|
||||
onSuccess: (token: string) => void
|
||||
) => {
|
||||
const onSubmit = useCallback<FormikConfig<SocialRegisterData>['onSubmit']>(
|
||||
async (values, { setErrors }) => {
|
||||
try {
|
||||
const result = await fetcher({
|
||||
token,
|
||||
username: values.username,
|
||||
password: values.password || '',
|
||||
});
|
||||
onSuccess(result.token);
|
||||
} catch (error) {
|
||||
showErrorToast(error);
|
||||
|
||||
const validationErrors = getValidationErrors(error);
|
||||
if (validationErrors) {
|
||||
setErrors(validationErrors);
|
||||
}
|
||||
}
|
||||
},
|
||||
[token, onSuccess, fetcher]
|
||||
);
|
||||
|
||||
return useFormik<SocialRegisterData>({
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
initialValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
});
|
||||
};
|
30
src/hooks/auth/useUser.ts
Normal file
30
src/hooks/auth/useUser.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import useSWR from 'swr';
|
||||
import { useAuthStore } from '~/store/auth/useAuthStore';
|
||||
import { API } from '~/constants/api';
|
||||
import { apiAuthGetUser } from '~/api/auth';
|
||||
import { EMPTY_USER } from '~/constants/auth';
|
||||
import { showErrorToast } from '~/utils/errors/showToast';
|
||||
import { useCallback } from 'react';
|
||||
import { IUser } from '~/types/auth';
|
||||
|
||||
export const useUser = () => {
|
||||
const { token, setUser } = useAuthStore();
|
||||
const { data, mutate } = useSWR(token ? API.USER.ME : null, () => apiAuthGetUser(), {
|
||||
onSuccess: data => setUser(data?.user || EMPTY_USER),
|
||||
onError: error => showErrorToast(error),
|
||||
});
|
||||
|
||||
const update = useCallback(
|
||||
async (user: Partial<IUser>, revalidate?: boolean) => {
|
||||
if (!data?.user) {
|
||||
console.warn('mutating user before loaded it :-(');
|
||||
return;
|
||||
}
|
||||
|
||||
await mutate({ ...data, user: { ...data.user, ...user } }, revalidate);
|
||||
},
|
||||
[data, mutate]
|
||||
);
|
||||
|
||||
return { user: data?.user || EMPTY_USER, update };
|
||||
};
|
21
src/hooks/auth/useUserDescription.ts
Normal file
21
src/hooks/auth/useUserDescription.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { IUser } from '~/types/auth';
|
||||
import { useRandomPhrase } from '~/constants/phrases';
|
||||
import { differenceInDays, parseISO } from 'date-fns';
|
||||
import { INACTIVE_ACCOUNT_DAYS } from '~/constants/user';
|
||||
|
||||
const today = new Date();
|
||||
|
||||
export const useUserDescription = (user?: Partial<IUser>) => {
|
||||
const randomPhrase = useRandomPhrase('USER_DESCRIPTION');
|
||||
|
||||
if (!user) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const lastSeen = user.last_seen ? parseISO(user.last_seen) : undefined;
|
||||
if (!lastSeen || differenceInDays(today, lastSeen) > INACTIVE_ACCOUNT_DAYS) {
|
||||
return 'Юнит деактивирован';
|
||||
}
|
||||
|
||||
return user.description || randomPhrase;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue