From 2c5ca0e2fb301a1659500364ef36d88fae0ecc6a Mon Sep 17 00:00:00 2001 From: Suvash Vishowkarma Date: Thu, 15 Feb 2024 14:54:10 +0545 Subject: [PATCH 1/3] feat(user): add useLogin hook --- .../src/components/Login/LoginWrapper.tsx | 51 +++------------ packages/user/src/hooks/index.ts | 1 + packages/user/src/hooks/useLogin.ts | 64 +++++++++++++++++++ packages/user/src/index.ts | 12 ++-- 4 files changed, 83 insertions(+), 45 deletions(-) create mode 100644 packages/user/src/hooks/useLogin.ts diff --git a/packages/user/src/components/Login/LoginWrapper.tsx b/packages/user/src/components/Login/LoginWrapper.tsx index c34e14d1a..53d4b8c84 100644 --- a/packages/user/src/components/Login/LoginWrapper.tsx +++ b/packages/user/src/components/Login/LoginWrapper.tsx @@ -1,22 +1,19 @@ import { useTranslation } from "@dzangolab/react-i18n"; -import { FC, useState } from "react"; -import { toast } from "react-toastify"; +import { FC } from "react"; import { LinkType } from "@/types/types"; import { LoginForm } from "./LoginForm"; import { ROUTES } from "../../constants"; -import { useConfig, useUser } from "../../hooks"; -import { verifySessionRoles } from "../../supertokens/helpers"; -import login from "../../supertokens/login"; import { AuthLinks } from "../AuthLinks"; +import { useConfig, useLogin } from "../../hooks"; import type { LoginCredentials, SignInUpPromise } from "../../types"; interface IProperties { handleSubmit?: (credential: LoginCredentials) => void; - onLoginFailed?: (error: Error) => void; - onLoginSuccess?: (user: SignInUpPromise) => void; + onLoginFailed?: (error: Error) => Promise | void; + onLoginSuccess?: (user: SignInUpPromise) => Promise | void; loading?: boolean; showForgotPasswordLink?: boolean; showSignupLink?: boolean; @@ -31,9 +28,12 @@ export const LoginWrapper: FC = ({ showSignupLink = true, }) => { const { t } = useTranslation(["user", "errors"]); - const { setUser } = useUser(); const { user: userConfig } = useConfig(); - const [loginLoading, setLoginLoading] = useState(false); + + const [loginLoading, loginUser] = useLogin({ + onLoginFailed, + onLoginSuccess, + }); const links: Array = [ { @@ -55,38 +55,7 @@ export const LoginWrapper: FC = ({ if (handleSubmit) { handleSubmit(credentials); } else { - setLoginLoading(true); - - await login(credentials) - .then(async (result) => { - if (result?.user) { - if ( - userConfig && - (await verifySessionRoles(userConfig.supportedRoles)) - ) { - setUser(result.user); - - onLoginSuccess && (await onLoginSuccess(result)); - - toast.success(`${t("login.messages.success")}`); - } else { - toast.error(t("login.messages.permissionDenied")); - } - } - }) - .catch(async (error) => { - let errorMessage = "errors.otherErrors"; - - if (error.message) { - errorMessage = `errors.${error.message}`; - } - - onLoginFailed && (await onLoginFailed(error)); - - toast.error(t(errorMessage, { ns: "errors" })); - }); - - setLoginLoading(false); + await loginUser(credentials); } }; diff --git a/packages/user/src/hooks/index.ts b/packages/user/src/hooks/index.ts index 4abf1270d..0f47eb658 100644 --- a/packages/user/src/hooks/index.ts +++ b/packages/user/src/hooks/index.ts @@ -3,3 +3,4 @@ export * from "./useEmailVerification"; export * from "./useFirstUserSignup"; export * from "./useProfileCompletion"; export * from "./useUser"; +export * from "./useLogin"; diff --git a/packages/user/src/hooks/useLogin.ts b/packages/user/src/hooks/useLogin.ts new file mode 100644 index 000000000..5ad27e02c --- /dev/null +++ b/packages/user/src/hooks/useLogin.ts @@ -0,0 +1,64 @@ +import { useTranslation } from "@dzangolab/react-i18n"; +import { useState } from "react"; +import { toast } from "react-toastify"; + +import { useConfig } from "./useConfig"; +import { useUser } from "./useUser"; +import { verifySessionRoles } from "../supertokens/helpers"; +import login from "../supertokens/login"; + +import type { LoginCredentials } from "../types"; + +type UseLoginConfig = { + showToasts?: boolean; + onLoginSuccess?: (user: any) => Promise | void; + onLoginFailed?: (error: any) => Promise | void; +}; + +export function useLogin(config?: UseLoginConfig) { + const { showToasts = true, onLoginFailed, onLoginSuccess } = config || {}; + + const { t } = useTranslation(["user", "errors"]); + const { setUser } = useUser(); + const { user: userConfig } = useConfig(); + + const [loginLoading, setLoginLoading] = useState(false); + + const loginUser = async (credentials: LoginCredentials) => { + setLoginLoading(true); + + await login(credentials) + .then(async (result) => { + if (result?.user) { + if ( + userConfig && + (await verifySessionRoles(userConfig.supportedRoles)) + ) { + setUser(result.user); + + onLoginSuccess && (await onLoginSuccess(result)); + + showToasts && toast.success(t("login.messages.success")); + } else { + showToasts && toast.error(t("login.messages.permissionDenied")); + } + } + }) + .catch(async (error) => { + let errorMessage = "errors.otherErrors"; + + if (error.message) { + errorMessage = `errors.${error.message}`; + } + + onLoginFailed && (await onLoginFailed(error)); + + showToasts && toast.error(t(errorMessage, { ns: "errors" })); + }); + }; + + return [loginLoading, loginUser] as [ + boolean, + (credentials: LoginCredentials) => Promise, + ]; +} diff --git a/packages/user/src/index.ts b/packages/user/src/index.ts index 69888827a..076d58aee 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -21,10 +21,11 @@ import { import UserProvider, { userContext } from "./context/UserProvider"; import { getUserData, removeUserData, setUserData } from "./helpers"; import { - useUser, useEmailVerification, useFirstUserSignup, + useLogin, useProfileCompletion, + useUser, } from "./hooks"; import { UserEnabledBasicLayout, @@ -129,14 +130,17 @@ export { setUserData, signup, superTokens, - useEmailVerification, - useFirstUserSignup, isProfileCompleted, useProfileCompletion, - useUser, userContext, verifyEmail, verifySessionRoles, + + // hooks + useEmailVerification, + useFirstUserSignup, + useLogin, + useUser, }; export type { From 5ef0de67827a9c0271040ebbd9106a7992e0d456 Mon Sep 17 00:00:00 2001 From: Suvash Vishowkarma Date: Fri, 16 Feb 2024 13:17:57 +0545 Subject: [PATCH 2/3] feat(user): add useAcceptInvitation hook --- .../src/components/Login/LoginWrapper.tsx | 2 +- packages/user/src/hooks/index.ts | 2 + .../user/src/hooks/useAcceptInvitation.ts | 103 ++++++++++++++++++ packages/user/src/hooks/useLogin.ts | 34 ++++-- 4 files changed, 128 insertions(+), 13 deletions(-) create mode 100644 packages/user/src/hooks/useAcceptInvitation.ts diff --git a/packages/user/src/components/Login/LoginWrapper.tsx b/packages/user/src/components/Login/LoginWrapper.tsx index 53d4b8c84..a536d90bf 100644 --- a/packages/user/src/components/Login/LoginWrapper.tsx +++ b/packages/user/src/components/Login/LoginWrapper.tsx @@ -30,7 +30,7 @@ export const LoginWrapper: FC = ({ const { t } = useTranslation(["user", "errors"]); const { user: userConfig } = useConfig(); - const [loginLoading, loginUser] = useLogin({ + const [loginUser, { isLoading: loginLoading }] = useLogin({ onLoginFailed, onLoginSuccess, }); diff --git a/packages/user/src/hooks/index.ts b/packages/user/src/hooks/index.ts index 0f47eb658..a1c335aa5 100644 --- a/packages/user/src/hooks/index.ts +++ b/packages/user/src/hooks/index.ts @@ -1,6 +1,8 @@ +export * from "./useAcceptInvitation"; export * from "./useConfig"; export * from "./useEmailVerification"; export * from "./useFirstUserSignup"; export * from "./useProfileCompletion"; export * from "./useUser"; export * from "./useLogin"; +export * from "./useUser"; diff --git a/packages/user/src/hooks/useAcceptInvitation.ts b/packages/user/src/hooks/useAcceptInvitation.ts new file mode 100644 index 000000000..573a94b76 --- /dev/null +++ b/packages/user/src/hooks/useAcceptInvitation.ts @@ -0,0 +1,103 @@ +import { useTranslation } from "@dzangolab/react-i18n"; +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { toast } from "react-toastify"; + +import { + acceptInvitation as acceptInvitationApi, + getInvitationByToken, +} from "@/api/invitation"; + +import { useConfig } from "./useConfig"; +import { useLogin } from "./useLogin"; + +import type { Invitation, LoginCredentials } from "../types"; + +type UseAcceptInvitationConfig = { + showToasts?: boolean; + tokenParamKey?: string; +}; + +type UseAcceptInvitationMeta = { + isError: boolean; + isFetching: boolean; + isLoading: boolean; + isLoginLoading: boolean; +}; + +export function useAcceptInvitation(config?: UseAcceptInvitationConfig) { + const { showToasts = true, tokenParamKey: tokenParameterKey = "token" } = + config || {}; + + const { t } = useTranslation("invitations"); + const appConfig: any = useConfig(); + + const parameters = useParams(); + const token = parameters[tokenParameterKey]; + + const [isError, setIsError] = useState(false); + const [isFetching, setIsFetching] = useState(false); + const [invitation, setInvitation] = useState(null); + const [loginUser, { isLoading: isLoginLoading }] = useLogin({ showToasts }); + + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (token) { + setIsFetching(true); + getInvitationByToken(token, appConfig?.apiBaseUrl || "") + .then((response) => { + if ("data" in response && response.data.status === "ERROR") { + // TODO better handle errors + setIsError(true); + } else { + setInvitation(response as Invitation); + } + }) + .catch(() => { + setIsError(true); + }) + .finally(() => { + setIsFetching(false); + }); + } + }, []); + + const acceptInvitation = (credentials: LoginCredentials) => { + if (!token) { + return; + } + + setIsLoading(true); + + return acceptInvitationApi(token, credentials, appConfig?.apiBaseUrl || "") + .then(async (response) => { + setIsLoading(false); + + if ("data" in response && response.data.status === "ERROR") { + // TODO better handle errors + setIsError(true); + showToasts && toast.error(response.data.message); + } else { + // TODO acceptInvitation should return authenticated user from api + await loginUser(credentials); + } + }) + .catch(() => { + setIsError(true); + setIsLoading(false); + showToasts && + toast.error(`${t("invitations.messages.errorAcceptingInvitation")}`); + }); + }; + + return [ + invitation, + acceptInvitation, + { isError, isFetching, isLoading, isLoginLoading }, + ] as [ + Invitation | null, + (credentials: LoginCredentials) => Promise, + UseAcceptInvitationMeta, + ]; +} diff --git a/packages/user/src/hooks/useLogin.ts b/packages/user/src/hooks/useLogin.ts index 5ad27e02c..2aec7d787 100644 --- a/packages/user/src/hooks/useLogin.ts +++ b/packages/user/src/hooks/useLogin.ts @@ -15,6 +15,11 @@ type UseLoginConfig = { onLoginFailed?: (error: any) => Promise | void; }; +type UseLoginMeta = { + isError: boolean; + isLoading: boolean; +}; + export function useLogin(config?: UseLoginConfig) { const { showToasts = true, onLoginFailed, onLoginSuccess } = config || {}; @@ -22,12 +27,13 @@ export function useLogin(config?: UseLoginConfig) { const { setUser } = useUser(); const { user: userConfig } = useConfig(); - const [loginLoading, setLoginLoading] = useState(false); + const [isError, setIsError] = useState(false); + const [isLoading, setIsLoading] = useState(false); - const loginUser = async (credentials: LoginCredentials) => { - setLoginLoading(true); + const loginUser = (credentials: LoginCredentials) => { + setIsLoading(true); - await login(credentials) + return login(credentials) .then(async (result) => { if (result?.user) { if ( @@ -40,25 +46,29 @@ export function useLogin(config?: UseLoginConfig) { showToasts && toast.success(t("login.messages.success")); } else { + setIsError(true); showToasts && toast.error(t("login.messages.permissionDenied")); } } }) .catch(async (error) => { - let errorMessage = "errors.otherErrors"; + setIsError(true); + onLoginFailed && (await onLoginFailed(error)); - if (error.message) { - errorMessage = `errors.${error.message}`; - } + if (showToasts) { + let errorMessage = "errors.otherErrors"; - onLoginFailed && (await onLoginFailed(error)); + if (error.message) { + errorMessage = `errors.${error.message}`; + } - showToasts && toast.error(t(errorMessage, { ns: "errors" })); + toast.error(t(errorMessage, { ns: "errors" })); + } }); }; - return [loginLoading, loginUser] as [ - boolean, + return [loginUser, { isError, isLoading }] as [ (credentials: LoginCredentials) => Promise, + UseLoginMeta, ]; } From 82defbefb24b8c5d9a1c6b14749406bc13670a4e Mon Sep 17 00:00:00 2001 From: Suvash Vishowkarma Date: Fri, 16 Feb 2024 14:11:13 +0545 Subject: [PATCH 3/3] feat(auth): add useChangePassword hook --- packages/i18n/src/locales/en/user.json | 1 + packages/i18n/src/locales/fr/user.json | 1 + packages/user/src/api/user/index.ts | 20 ++++ .../src/components/Login/LoginWrapper.tsx | 4 +- packages/user/src/hooks/index.ts | 1 + .../user/src/hooks/useAcceptInvitation.ts | 28 +++++- packages/user/src/hooks/useChangePassword.ts | 64 +++++++++++++ packages/user/src/hooks/useLogin.ts | 18 ++-- packages/user/src/index.ts | 41 ++++++-- .../user/src/supertokens/change-password.ts | 37 ------- packages/user/src/types/index.ts | 2 + packages/user/src/types/types.ts | 5 + packages/user/src/views/AcceptInvitation.tsx | 96 +++---------------- packages/user/src/views/ChangePassword.tsx | 27 ++---- 14 files changed, 182 insertions(+), 163 deletions(-) create mode 100644 packages/user/src/hooks/useChangePassword.ts delete mode 100644 packages/user/src/supertokens/change-password.ts diff --git a/packages/i18n/src/locales/en/user.json b/packages/i18n/src/locales/en/user.json index 82dc52244..7e4805f74 100644 --- a/packages/i18n/src/locales/en/user.json +++ b/packages/i18n/src/locales/en/user.json @@ -15,6 +15,7 @@ } }, "messages": { + "error": "Could not change your password.", "success": "Your password was successfully changed.", "validation": { "oldPassword": "Old password is required", diff --git a/packages/i18n/src/locales/fr/user.json b/packages/i18n/src/locales/fr/user.json index c893eb796..130ef5ce8 100644 --- a/packages/i18n/src/locales/fr/user.json +++ b/packages/i18n/src/locales/fr/user.json @@ -15,6 +15,7 @@ } }, "messages": { + "error": "Could not change your password. (fr)", "success": "Your password was successfully changed. (fr)", "validation": { "oldPassword": "Old password is required (fr)", diff --git a/packages/user/src/api/user/index.ts b/packages/user/src/api/user/index.ts index 4f3b33fa3..085a81517 100644 --- a/packages/user/src/api/user/index.ts +++ b/packages/user/src/api/user/index.ts @@ -1,11 +1,31 @@ import client from "../axios"; import type { + ChangePasswordInputType, LoginCredentials, UpdateProfileInputType, UserType, } from "@/types"; +export const changePassword = async ( + { newPassword, oldPassword }: ChangePasswordInputType, + apiBaseUrl: string, +) => { + const response = await client(apiBaseUrl).post( + "/change_password", + { oldPassword, newPassword }, + { + withCredentials: true, + }, + ); + + if (response.data.status === "ERROR") { + throw new Error(response.data.message); + } else { + return response; + } +}; + export const getIsFirstUser = async ( apiBaseUrl: string, ): Promise<{ diff --git a/packages/user/src/components/Login/LoginWrapper.tsx b/packages/user/src/components/Login/LoginWrapper.tsx index a536d90bf..89e782bb8 100644 --- a/packages/user/src/components/Login/LoginWrapper.tsx +++ b/packages/user/src/components/Login/LoginWrapper.tsx @@ -31,8 +31,8 @@ export const LoginWrapper: FC = ({ const { user: userConfig } = useConfig(); const [loginUser, { isLoading: loginLoading }] = useLogin({ - onLoginFailed, - onLoginSuccess, + onSuccess: onLoginFailed, + onFailed: onLoginSuccess, }); const links: Array = [ diff --git a/packages/user/src/hooks/index.ts b/packages/user/src/hooks/index.ts index a1c335aa5..264002f23 100644 --- a/packages/user/src/hooks/index.ts +++ b/packages/user/src/hooks/index.ts @@ -1,4 +1,5 @@ export * from "./useAcceptInvitation"; +export * from "./useChangePassword"; export * from "./useConfig"; export * from "./useEmailVerification"; export * from "./useFirstUserSignup"; diff --git a/packages/user/src/hooks/useAcceptInvitation.ts b/packages/user/src/hooks/useAcceptInvitation.ts index 573a94b76..22b22e95b 100644 --- a/packages/user/src/hooks/useAcceptInvitation.ts +++ b/packages/user/src/hooks/useAcceptInvitation.ts @@ -14,8 +14,11 @@ import { useLogin } from "./useLogin"; import type { Invitation, LoginCredentials } from "../types"; type UseAcceptInvitationConfig = { + autoLogin?: boolean; showToasts?: boolean; tokenParamKey?: string; + onSuccess?: (user: any) => Promise | void; + onFailed?: (error?: any) => Promise | void; }; type UseAcceptInvitationMeta = { @@ -26,8 +29,13 @@ type UseAcceptInvitationMeta = { }; export function useAcceptInvitation(config?: UseAcceptInvitationConfig) { - const { showToasts = true, tokenParamKey: tokenParameterKey = "token" } = - config || {}; + const { + autoLogin = true, + showToasts = true, + tokenParamKey: tokenParameterKey = "token", + onSuccess, + onFailed, + } = config || {}; const { t } = useTranslation("invitations"); const appConfig: any = useConfig(); @@ -77,15 +85,25 @@ export function useAcceptInvitation(config?: UseAcceptInvitationConfig) { if ("data" in response && response.data.status === "ERROR") { // TODO better handle errors setIsError(true); + + onFailed && (await onFailed(response)); + showToasts && toast.error(response.data.message); } else { - // TODO acceptInvitation should return authenticated user from api - await loginUser(credentials); + onSuccess && (await onSuccess(response)); + + if (autoLogin) { + // TODO acceptInvitation should return authenticated user from api + await loginUser(credentials); + } } }) - .catch(() => { + .catch(async (error) => { setIsError(true); setIsLoading(false); + + onFailed && (await onFailed(error)); + showToasts && toast.error(`${t("invitations.messages.errorAcceptingInvitation")}`); }); diff --git a/packages/user/src/hooks/useChangePassword.ts b/packages/user/src/hooks/useChangePassword.ts new file mode 100644 index 000000000..56a1d8642 --- /dev/null +++ b/packages/user/src/hooks/useChangePassword.ts @@ -0,0 +1,64 @@ +import { useTranslation } from "@dzangolab/react-i18n"; +import { useState } from "react"; +import { toast } from "react-toastify"; + +import { useConfig } from "./useConfig"; +import { changePassword as changePasswordApi } from "../api/user"; + +import type { ChangePasswordInputType } from "../types"; + +type UseChangePasswordConfig = { + showToasts?: boolean; + onSuccess?: (user: any) => Promise | void; + onFailed?: (error: any) => Promise | void; +}; + +type UseChangePasswordMeta = { + isError: boolean; + isLoading: boolean; +}; + +export function useChangePassword(config?: UseChangePasswordConfig) { + const { showToasts = true, onFailed, onSuccess } = config || {}; + + const { t } = useTranslation(["user", "errors"]); + const appConfig = useConfig(); + + const [isError, setIsError] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const changePassword = (passwords: ChangePasswordInputType) => { + setIsLoading(true); + + return changePasswordApi(passwords, appConfig?.apiBaseUrl || "") + .then(async (response) => { + setIsLoading(false); + + if ("data" in response && response.data.status === "OK") { + onSuccess && (await onSuccess(response)); + + showToasts && toast.success(t("changePassword.messages.success")); + } else { + // TODO better handle errors + setIsError(true); + + onFailed && (await onFailed(response)); + + showToasts && toast.error(response.data.message); + } + }) + .catch(async (error) => { + setIsError(true); + setIsLoading(false); + + onFailed && (await onFailed(error)); + + showToasts && toast.error(`${t("changePassword.messages.error")}`); + }); + }; + + return [changePassword, { isError, isLoading }] as [ + (passwords: ChangePasswordInputType) => Promise, + UseChangePasswordMeta, + ]; +} diff --git a/packages/user/src/hooks/useLogin.ts b/packages/user/src/hooks/useLogin.ts index 2aec7d787..d3d8140d5 100644 --- a/packages/user/src/hooks/useLogin.ts +++ b/packages/user/src/hooks/useLogin.ts @@ -11,8 +11,8 @@ import type { LoginCredentials } from "../types"; type UseLoginConfig = { showToasts?: boolean; - onLoginSuccess?: (user: any) => Promise | void; - onLoginFailed?: (error: any) => Promise | void; + onSuccess?: (user: any) => Promise | void; + onFailed?: (error?: any) => Promise | void; }; type UseLoginMeta = { @@ -21,7 +21,7 @@ type UseLoginMeta = { }; export function useLogin(config?: UseLoginConfig) { - const { showToasts = true, onLoginFailed, onLoginSuccess } = config || {}; + const { showToasts = true, onFailed, onSuccess } = config || {}; const { t } = useTranslation(["user", "errors"]); const { setUser } = useUser(); @@ -34,26 +34,28 @@ export function useLogin(config?: UseLoginConfig) { setIsLoading(true); return login(credentials) - .then(async (result) => { - if (result?.user) { + .then(async (response) => { + if (response?.user) { if ( userConfig && (await verifySessionRoles(userConfig.supportedRoles)) ) { - setUser(result.user); + setUser(response.user); - onLoginSuccess && (await onLoginSuccess(result)); + onSuccess && (await onSuccess(response)); showToasts && toast.success(t("login.messages.success")); } else { setIsError(true); + onFailed && (await onFailed(response)); + showToasts && toast.error(t("login.messages.permissionDenied")); } } }) .catch(async (error) => { setIsError(true); - onLoginFailed && (await onLoginFailed(error)); + onFailed && (await onFailed(error)); if (showToasts) { let errorMessage = "errors.otherErrors"; diff --git a/packages/user/src/index.ts b/packages/user/src/index.ts index 076d58aee..2fa44dd43 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -1,6 +1,19 @@ import { sendPasswordResetEmail } from "supertokens-web-js/recipe/thirdpartyemailpassword"; import { disableUser, enableUser } from "./api/user"; +import { + acceptInvitation, + addInvitation, + getInvitationByToken, + resendInvitation, + revokeInvitation, +} from "./api/invitation"; +import { + changePassword, + getIsFirstUser, + signUpFirstUser, + updateUserProfile, +} from "./api/user"; import AuthGoogleCallback from "./components/AuthGoogleCallback"; import DropdownUserMenu from "./components/DropdownUserMenu"; import { @@ -21,6 +34,8 @@ import { import UserProvider, { userContext } from "./context/UserProvider"; import { getUserData, removeUserData, setUserData } from "./helpers"; import { + useAcceptInvitation, + useChangePassword, useEmailVerification, useFirstUserSignup, useLogin, @@ -35,7 +50,6 @@ import { UserEnabledSidebarOnlyLayout, } from "./layouts"; import superTokens from "./supertokens"; -import changePassword from "./supertokens/change-password"; import { forgotPassword } from "./supertokens/forgot-password"; import googleLogin from "./supertokens/google-login"; import { verifySessionRoles, isProfileCompleted } from "./supertokens/helpers"; @@ -91,9 +105,12 @@ export { SignupWrapper, TermsAndConditions, UserMenu, - UserProvider, UsersTable, + // context + userContext, + UserProvider, + // layouts UserEnabledBasicLayout, UserEnabledHeaderLayout, @@ -115,9 +132,7 @@ export { VerifyEmail, // utilities - changePassword, - disableUser, - enableUser, + // supertoken utilities forgotPassword, getUserData, googleLogin, @@ -132,15 +147,29 @@ export { superTokens, isProfileCompleted, useProfileCompletion, - userContext, verifyEmail, verifySessionRoles, // hooks + useAcceptInvitation, + useChangePassword, useEmailVerification, useFirstUserSignup, useLogin, useUser, + + // apis + changePassword, + disableUser, + enableUser, + getIsFirstUser, + signUpFirstUser, + updateUserProfile, + acceptInvitation, + addInvitation, + getInvitationByToken, + resendInvitation, + revokeInvitation, }; export type { diff --git a/packages/user/src/supertokens/change-password.ts b/packages/user/src/supertokens/change-password.ts deleted file mode 100644 index 1a79aa477..000000000 --- a/packages/user/src/supertokens/change-password.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { toast } from "react-toastify"; - -import client from "@/api/axios"; - -const changePassword = async ( - oldPassword: string, - newPassword: string, - apiBaseUrl: string, -): Promise => { - let success = false; - - try { - const response = await client(apiBaseUrl).post( - "/change_password", - { oldPassword, newPassword }, - { - withCredentials: true, - }, - ); - - if (response.data.status === "OK") { - success = true; - } else { - toast.error(response.data.message); - } - } catch (err) { - let errorMessage = "Oops! Something went wrong."; - if (err instanceof Error) { - errorMessage = err.message; - } - toast.error(errorMessage); - } - - return success; -}; - -export default changePassword; diff --git a/packages/user/src/types/index.ts b/packages/user/src/types/index.ts index f1f4f20e2..a63e7b0c5 100644 --- a/packages/user/src/types/index.ts +++ b/packages/user/src/types/index.ts @@ -13,6 +13,7 @@ import { } from "./invitation"; import { AuthState, + ChangePasswordInputType, ErrorResponse, ExtendedUser, LoginCredentials, @@ -27,6 +28,7 @@ export type { AcceptInvitationResponse, AddInvitationResponse, AuthState, + ChangePasswordInputType, DzangolabReactUserConfig, ErrorResponse, ExtendedUser, diff --git a/packages/user/src/types/types.ts b/packages/user/src/types/types.ts index 3115e0d4f..da7e9078c 100644 --- a/packages/user/src/types/types.ts +++ b/packages/user/src/types/types.ts @@ -62,4 +62,9 @@ export type LinkType = { display?: boolean; label: string; to: string; +} + +export type ChangePasswordInputType = { + newPassword: string; + oldPassword: string; }; diff --git a/packages/user/src/views/AcceptInvitation.tsx b/packages/user/src/views/AcceptInvitation.tsx index 28ccf6abe..c1f75c430 100644 --- a/packages/user/src/views/AcceptInvitation.tsx +++ b/packages/user/src/views/AcceptInvitation.tsx @@ -1,16 +1,10 @@ import { useTranslation } from "@dzangolab/react-i18n"; import { AuthPage } from "@dzangolab/react-ui"; import { Card } from "primereact/card"; -import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; -import { toast } from "react-toastify"; -import { acceptInvitation, getInvitationByToken } from "@/api/invitation"; import SignupForm from "@/components/SignupForm"; -import { useConfig, useUser } from "@/hooks"; -import { Invitation, LoginCredentials } from "@/types"; - -import { login } from ".."; +import { useAcceptInvitation } from "@/hooks"; +import { LoginCredentials } from "@/types"; export const AcceptInvitation = ({ centered = true, @@ -19,82 +13,14 @@ export const AcceptInvitation = ({ }) => { const { t } = useTranslation("invitations"); - const appConfig = useConfig(); - const { token } = useParams(); - const { setUser } = useUser(); - - const [loading, setLoading] = useState(false); - const [isError, setIsError] = useState(false); - const [invitation, setInvitation] = useState(null); - - const [acceptInvitationLoading, setAcceptInvitationLoading] = useState(false); - const [loginLoading, setLoginLoading] = useState(false); - - useEffect(() => { - if (token) { - getInvitationByToken(token, appConfig?.apiBaseUrl || "") - .then((response) => { - if ("data" in response && response.data.status === "ERROR") { - // TODO better handle errors - setIsError(true); - } else { - setInvitation(response as Invitation); - } - }) - .catch(() => { - setIsError(true); - }) - .finally(() => { - setLoading(false); - }); - } - }, []); - - const handleSubmit = (credentials: LoginCredentials) => { - if (!token) { - return; - } - - setAcceptInvitationLoading(true); - - acceptInvitation(token, credentials, appConfig?.apiBaseUrl || "") - .then((response) => { - setAcceptInvitationLoading(false); - - if ("data" in response && response.data.status === "ERROR") { - // TODO better handle errors - toast.error(response.data.message); - } else { - setLoginLoading(true); - - // TODO acceptInvitation should return authenticated user from api - login(credentials) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .then((result: any) => { - if (result?.user) { - setUser(result.user); - toast.success(`${t("user:login.messages.success")}`); - } - }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .catch((error: any) => { - const errorMessage = t("errors:errors.otherErrors"); - - if (error.name) { - throw error as Error; - } + const [ + invitation, + acceptInvitation, + { isError, isFetching, isLoading, isLoginLoading }, + ] = useAcceptInvitation(); - toast.error(error.message || errorMessage); - }) - .finally(() => { - setLoginLoading(false); - }); - } - }) - .catch(() => { - setAcceptInvitationLoading(false); - toast.error(`${t("invitations.messages.errorAcceptingInvitation")}`); - }); + const handleSubmit = async (credentials: LoginCredentials) => { + await acceptInvitation(credentials); }; const renderPageContent = () => { @@ -123,7 +49,7 @@ export const AcceptInvitation = ({ key={invitation.id} email={invitation.email || ""} handleSubmit={handleSubmit} - loading={acceptInvitationLoading} + loading={isLoading} /> ); }; @@ -132,8 +58,8 @@ export const AcceptInvitation = ({ {renderPageContent()} diff --git a/packages/user/src/views/ChangePassword.tsx b/packages/user/src/views/ChangePassword.tsx index d502e8a49..471477d55 100644 --- a/packages/user/src/views/ChangePassword.tsx +++ b/packages/user/src/views/ChangePassword.tsx @@ -1,31 +1,18 @@ import { useTranslation } from "@dzangolab/react-i18n"; import { AuthPage } from "@dzangolab/react-ui"; -import React, { useState } from "react"; -import { toast } from "react-toastify"; +import React from "react"; + +import { useChangePassword } from "@/hooks/useChangePassword"; import ChangePasswordForm from "../components/ChangePasswordForm"; -import { useConfig } from "../hooks"; -import changePassword from "../supertokens/change-password"; export const ChangePassword = ({ centered = true }: { centered?: boolean }) => { const { t } = useTranslation("user"); - const appConfig = useConfig(); - const [loading, setLoading] = useState(false); - - const handleSubmit = async (oldPassword: string, newPassword: string) => { - setLoading(true); - const success = await changePassword( - oldPassword, - newPassword, - appConfig?.apiBaseUrl || "", - ); + const [changePassword, { isLoading }] = useChangePassword(); - if (success) { - toast.success(t("changePassword.messages.success")); - } - - setLoading(false); + const handleSubmit = async (oldPassword: string, newPassword: string) => { + await changePassword({ newPassword, oldPassword }); }; return ( @@ -34,7 +21,7 @@ export const ChangePassword = ({ centered = true }: { centered?: boolean }) => { title={t("changePassword.title")} centered={centered} > - + ); };