1
0
Fork 0
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:
Fedor Katurov 2022-01-09 19:03:01 +07:00
parent 26e6d8d41b
commit a4bb07e9cf
323 changed files with 2464 additions and 3348 deletions

20
src/hooks/auth/useAuth.ts Normal file
View 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,
};
};

View 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 };
};

View 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: '',
},
});
};

View 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 };
};

View 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
View 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,
};
};

View 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 };
};

View 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) };
};

View 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: '' },
});
};

View 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]);
};

View 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: '' },
});
};

View 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
View 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 };
};

View 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;
};