diff --git a/src/components/Auth/index.tsx b/src/components/Auth/index.tsx index 43976edc..e98cf18f 100644 --- a/src/components/Auth/index.tsx +++ b/src/components/Auth/index.tsx @@ -8,6 +8,7 @@ import { matchesPathname } from '../../utils/matchesPathname'; import { FleekLogo } from '../FleekLogo/FleekLogo'; import type { FC, ReactNode } from 'react'; +import { useAuthContext } from '@/providers/AuthProvider'; interface AuthProps { children: ReactNode; @@ -15,24 +16,22 @@ interface AuthProps { export const Auth: FC = ({ children }) => { const router = useRouter(); + const auth = useAuthContext(); const [isChecking, setIsChecking] = useState(true); useEffect(() => { const checkAuth = () => { - const authToken = document.cookie + const authProviderToken = document.cookie .split('; ') .find((row) => row.startsWith('authProviderToken=')) ?.split('=')[1]; - const projectId = - document.cookie - .split('; ') - .find((row) => row.startsWith('projectId=')) - ?.split('=')[1] || constants.DEFAULT_PROJECT_ID; - const hasAuthentication = Boolean(authToken); + const hasAuthentication = Boolean(authProviderToken); const currentPath = window.location.pathname; if (hasAuthentication && currentPath === routes.home()) { - router.push(routes.project.home({ projectId })); + router.push( + routes.project.home({ projectId: constants.DEFAULT_PROJECT_ID }), + ); setIsChecking(false); return; @@ -45,7 +44,8 @@ export const Auth: FC = ({ children }) => { ); if (!hasAuthentication && !isPublicRoute) { - router.push(routes.home()); + console.log('logging out from auth'); + auth.logout(); } setIsChecking(false); diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index 9127a85a..778aabc0 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -7,7 +7,6 @@ import { useCreateProjectMutation, } from '@/generated/graphqlClient'; import { useToast } from '@/hooks/useToast'; -import { useCookies } from '@/providers/CookiesProvider'; import { useProjectContext } from '@/providers/ProjectProvider'; import { Button, Dialog, Text } from '@/ui'; @@ -15,13 +14,14 @@ import { Form } from '../Form/Form'; import { LearnMoreMessage } from '../LearnMoreMessage/LearnMoreMessage'; import { Modal } from '../Modal/Modal'; import { ProjectField } from '../ProjectField/ProjectField'; +import { useSessionContext } from '@/providers/SessionProvider'; export const CreateProject: React.FC = () => { + const session = useSessionContext(); const { isCreateProjectModalOpen: isModalOpen, setIsCreateProjectModalOpen } = useProjectContext(); const toast = useToast(); - const cookies = useCookies(); const client = useClient(); const [, createProject] = useCreateProjectMutation(); @@ -63,7 +63,7 @@ export const CreateProject: React.FC = () => { }); } - cookies.set('projectId', data.createProject.id); + session.setProject(data.createProject.id); handleModalChange(false); } catch (error) { toast.error({ diff --git a/src/components/FeedbackModal/FeedbackModal.tsx b/src/components/FeedbackModal/FeedbackModal.tsx index cfaa7909..0716b110 100644 --- a/src/components/FeedbackModal/FeedbackModal.tsx +++ b/src/components/FeedbackModal/FeedbackModal.tsx @@ -24,6 +24,7 @@ import { SubmitSupportFormError, uploadFile, } from './submitForm'; +import { useAuthContext } from '@/providers/AuthProvider'; const MAX_FILE_SIZE_MB = 5; const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024; @@ -33,13 +34,14 @@ export const formSchema = zod.object({ }); export const FeedbackModal: React.FC = () => { + const auth = useAuthContext(); const feedbackModal = useFeedbackModal(); const [inputValue, setInputValue] = useState(''); const [view, setView] = useState<'FORM' | 'SUBMITTED'>('FORM'); const [files, setFiles] = useState([]); const toast = useToast(); - const [meQuery] = useMeQuery(); + const [meQuery] = useMeQuery({ variables: {}, pause: !auth.token }); const user = meQuery.data?.user; const isAuthed = !!user; diff --git a/src/constants.ts b/src/constants.ts index 5c11faf4..4d29ffb4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -11,7 +11,7 @@ export const constants = { VERSION: Package.version, FIRST_PROJECT_NAME: 'First Project', - DEFAULT_PROJECT_ID: 'new-project', + DEFAULT_PROJECT_ID: '[projectid]', EXTERNAL_LINK: { ENS_DOMAIN: 'https://app.ens.domains', diff --git a/src/fragments/App/Navbar/Navbar.tsx b/src/fragments/App/Navbar/Navbar.tsx index 46917927..ad5da704 100644 --- a/src/fragments/App/Navbar/Navbar.tsx +++ b/src/fragments/App/Navbar/Navbar.tsx @@ -8,6 +8,7 @@ import { Button, ButtonProps, Input } from '@/ui'; import { NavbarStyles as S } from './Navbar.styles'; import { Navigation } from './Navigation'; +import { constants } from '@/constants'; const Search: React.FC = () => { const handleSearch = (/* e: React.ChangeEvent */) => { @@ -68,7 +69,14 @@ const LoginButton: React.FC = ({ title, intent }) => { const session = useSessionContext(); return ( - ); diff --git a/src/fragments/App/Navbar/UserMenu/UserMenu.tsx b/src/fragments/App/Navbar/UserMenu/UserMenu.tsx index 5ec0a86f..4014a06a 100644 --- a/src/fragments/App/Navbar/UserMenu/UserMenu.tsx +++ b/src/fragments/App/Navbar/UserMenu/UserMenu.tsx @@ -29,7 +29,7 @@ export const UserMenu: React.FC = () => { const { theme, toggleTheme } = useTheme(); const session = useSessionContext(); const projectId = session.project.id; - const [meQuery] = useMeQuery(); + const [meQuery] = useMeQuery({ pause: !session.auth.token }); const handleCreateProjectClick = (): void => { projectContext.setIsCreateProjectModalOpen(true); diff --git a/src/fragments/IpfsPropagation/Sections/CreateGatewayButton.tsx b/src/fragments/IpfsPropagation/Sections/CreateGatewayButton.tsx index 0c3a2218..445ebaeb 100644 --- a/src/fragments/IpfsPropagation/Sections/CreateGatewayButton.tsx +++ b/src/fragments/IpfsPropagation/Sections/CreateGatewayButton.tsx @@ -12,7 +12,6 @@ export const CreateGatewayButton: React.FC = () => { const handleClick = () => { if (!hasToken) { session.auth.login( - 'dynamic', routes.project.settings.privateGateways({ projectId: 'project' }), ); } diff --git a/src/fragments/Migration/Layout.tsx b/src/fragments/Migration/Layout.tsx index ab23e93a..892e15ed 100644 --- a/src/fragments/Migration/Layout.tsx +++ b/src/fragments/Migration/Layout.tsx @@ -17,7 +17,7 @@ export const Layout: React.FC = ({ children }) => { const handleLogIn = () => { if (!session.error && !session.loading && !session.auth.token) { - session.auth.login('dynamic', routes.migration()); + session.auth.login(routes.migration()); } }; diff --git a/src/fragments/Profile/Settings/Sections/ManageProjects.tsx b/src/fragments/Profile/Settings/Sections/ManageProjects.tsx index 4709f013..f5a31cad 100644 --- a/src/fragments/Profile/Settings/Sections/ManageProjects.tsx +++ b/src/fragments/Profile/Settings/Sections/ManageProjects.tsx @@ -1,4 +1,3 @@ -import { routes } from '@fleek-platform/utils-routes'; import { useMemo, useState } from 'react'; import { BadgeText, SettingsBox, SettingsListItem } from '@/components'; @@ -9,15 +8,13 @@ import { useMeQuery, useProjectsQuery, } from '@/generated/graphqlClient'; -import { useRouter } from '@/hooks/useRouter'; import { useToast } from '@/hooks/useToast'; -import { useCookies } from '@/providers/CookiesProvider'; import { Icon } from '@/ui'; import { firstLetterUpperCase } from '@/utils/stringFormat'; +import { useSessionContext } from '@/providers/SessionProvider'; export const ManageProjects: React.FC = () => { - const router = useRouter(); - const cookies = useCookies(); + const session = useSessionContext(); const toast = useToast(); const [meQuery] = useMeQuery(); const [projectsQuery] = useProjectsQuery(); @@ -46,8 +43,7 @@ export const ManageProjects: React.FC = () => { } const handleViewProject = ({ projectId }: HandleViewProjectProps) => { - cookies.set('projectId', projectId); - router.push(routes.project.home({ projectId })); + session.setProject(projectId); }; const handleLeaveProject = async ({ projectId }: HandleLeaveProjectProps) => { diff --git a/src/fragments/Projects/Home/Sections/MainSites.tsx b/src/fragments/Projects/Home/Sections/MainSites.tsx index 57b53ed7..953b9557 100644 --- a/src/fragments/Projects/Home/Sections/MainSites.tsx +++ b/src/fragments/Projects/Home/Sections/MainSites.tsx @@ -9,8 +9,10 @@ import { } from '@/generated/graphqlClient'; import { useSessionContext } from '@/providers/SessionProvider'; import { Box, Icon, Skeleton, Text } from '@/ui'; +import { useAuthContext } from '@/providers/AuthProvider'; export const MainSites: React.FC = () => { + const auth = useAuthContext(); const session = useSessionContext(); const projectId = session.project.id; @@ -25,6 +27,7 @@ export const MainSites: React.FC = () => { sortOrder: SortOrder.desc, }, }, + pause: !auth.tokenProjectId, }); const sites = sitesQuery?.data?.sites.data || []; diff --git a/src/fragments/Projects/Settings/Sections/DeleteProject/DeleteProject.tsx b/src/fragments/Projects/Settings/Sections/DeleteProject/DeleteProject.tsx index 4c1af7f5..0ae17ab0 100644 --- a/src/fragments/Projects/Settings/Sections/DeleteProject/DeleteProject.tsx +++ b/src/fragments/Projects/Settings/Sections/DeleteProject/DeleteProject.tsx @@ -29,7 +29,6 @@ import { usePermissions } from '@/hooks/usePermissions'; import { useRouter } from '@/hooks/useRouter'; import { useToast } from '@/hooks/useToast'; import { useBillingContext } from '@/providers/BillingProvider'; -import { useCookies } from '@/providers/CookiesProvider'; import { useSessionContext } from '@/providers/SessionProvider'; import { ChildrenProps, LoadingProps } from '@/types/Props'; import { Button, Dialog, Text } from '@/ui'; @@ -53,7 +52,7 @@ export const DeleteProject: React.FC = ({ const client = useClient(); const router = useRouter(); const toast = useToast(); - const cookies = useCookies(); + const session = useSessionContext(); const [, createProject] = useCreateProjectMutation(); @@ -127,7 +126,7 @@ export const DeleteProject: React.FC = ({ } if (redirectProjectId) { - cookies.set('projectId', redirectProjectId); + session.setProject(redirectProjectId); } toast.success({ diff --git a/src/fragments/Projects/Sites/AddSiteDropdown.tsx b/src/fragments/Projects/Sites/AddSiteDropdown.tsx index 3bfc4ec1..6a6c4867 100644 --- a/src/fragments/Projects/Sites/AddSiteDropdown.tsx +++ b/src/fragments/Projects/Sites/AddSiteDropdown.tsx @@ -8,8 +8,10 @@ import { useSiteRestriction } from '@/hooks/useBillingRestriction'; import { usePermissions } from '@/hooks/usePermissions'; import { useSessionContext } from '@/providers/SessionProvider'; import { Button, Menu, Skeleton } from '@/ui'; +import { useAuthContext } from '@/providers/AuthProvider'; export const AddSiteDropdown: React.FC = () => { + const auth = useAuthContext(); const session = useSessionContext(); const hasDeployPermissions = usePermissions({ action: [constants.PERMISSION.SITE.CREATE], @@ -27,6 +29,7 @@ export const AddSiteDropdown: React.FC = () => { where: {}, filter: { take: constants.SITES_PAGE_SIZE, page: 1 }, }, + pause: !auth.tokenProjectId, }); if (sitesQuery.fetching) { diff --git a/src/fragments/Template/List/Hero/Hero.tsx b/src/fragments/Template/List/Hero/Hero.tsx index d5977643..9d35f4f0 100644 --- a/src/fragments/Template/List/Hero/Hero.tsx +++ b/src/fragments/Template/List/Hero/Hero.tsx @@ -41,7 +41,7 @@ const SubmitTemplateButton: React.FC = () => { const handleSubmitTemplate = () => { if (!session.auth.token) { - return session.auth.login('dynamic', routes.profile.settings.templates()); + return session.auth.login(routes.profile.settings.templates()); } return router.push(routes.profile.settings.templates()); diff --git a/src/fragments/Template/TemplateDetails/TemplateDetails.tsx b/src/fragments/Template/TemplateDetails/TemplateDetails.tsx index ff091709..1b2fbedc 100644 --- a/src/fragments/Template/TemplateDetails/TemplateDetails.tsx +++ b/src/fragments/Template/TemplateDetails/TemplateDetails.tsx @@ -8,6 +8,7 @@ import { getLinkForTemplateReport } from '@/utils/getLinkForTemplateReport'; import { firstLetterUpperCase } from '@/utils/stringFormat'; import { TemplateDetailsStyles as S } from './TemplateDetails.styles'; +import { useAuthContext } from '@/providers/AuthProvider'; export type TemplateDetailsProps = LoadingProps<{ template: Template }>; @@ -15,7 +16,8 @@ export const TemplateDetails: React.FC = ({ isLoading, template, }) => { - const [meQuery] = useMeQuery(); + const auth = useAuthContext(); + const [meQuery] = useMeQuery({ variables: {}, pause: !auth.token }); if (isLoading) { return ; diff --git a/src/hooks/useAuthProviders.ts b/src/hooks/useAuthProviders.ts deleted file mode 100644 index a23c240e..00000000 --- a/src/hooks/useAuthProviders.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { useDynamicContext } from '@dynamic-labs/sdk-react-core'; - -import { useLoginWithDynamicMutation } from '@/generated/graphqlClient'; -import { useCookies } from '@/providers/CookiesProvider'; -import { secrets } from '@/secrets'; - -export type AuthProviders = 'dynamic'; - -export type AuthWith = { - handleLogin: () => void; - handleLogout: () => void; - requestAccessToken: (projectId?: string) => Promise; - token: string | undefined; -}; - -export const useAuthProviders = (): Record => { - const dynamic = useAuthWithDynamic(); - - return { - dynamic, - ...(secrets.TEST_MODE ? { mocked: getMockedProvider() } : {}), - }; -}; - -const useAuthWithDynamic = (): AuthWith => { - const dynamic = useDynamicContext(); - - const [, loginWithDynamic] = useLoginWithDynamicMutation(); - - const handleLogin = () => dynamic.setShowAuthFlow(true); - - const handleLogout = () => dynamic.handleLogOut(); - - const requestAccessToken = async (projectId?: string): Promise => { - if (!dynamic.authToken) { - return ''; - } - - const { data, error } = await loginWithDynamic({ - data: { authToken: dynamic.authToken, projectId }, - }); - - if (data && data.loginWithDynamic) { - return data.loginWithDynamic; - } - - throw error; - }; - - return { - handleLogin, - handleLogout, - requestAccessToken, - token: dynamic.authToken, - }; -}; - -const getMockedProvider: () => AuthWith = () => { - // eslint-disable-next-line react-hooks/rules-of-hooks - const cookies = useCookies(); - - return { - handleLogin: () => {}, - handleLogout: () => {}, - requestAccessToken: async () => 'mocked-token', - token: cookies.values.authProviderToken, - }; -}; diff --git a/src/hooks/useBillingRestriction.tsx b/src/hooks/useBillingRestriction.tsx index f9da1ce7..e3a57ad4 100644 --- a/src/hooks/useBillingRestriction.tsx +++ b/src/hooks/useBillingRestriction.tsx @@ -9,8 +9,10 @@ import { useSessionContext } from '@/providers/SessionProvider'; import { filterDeletedDomains } from '@/utils/filterDeletedDomains'; import { useRouter } from './useRouter'; +import { useAuthContext } from '@/providers/AuthProvider'; export const useSiteRestriction = () => { + const auth = useAuthContext(); const billing = useBillingContext(); const [sitesQuery] = useSitesQuery({ @@ -18,6 +20,7 @@ export const useSiteRestriction = () => { where: {}, filter: { take: constants.SITES_PAGE_SIZE, page: 1 }, }, + pause: !auth.tokenProjectId, }); return billing.hasReachedLimit( diff --git a/src/hooks/useGetTeam.ts b/src/hooks/useGetTeam.ts index 265104ed..76b8546d 100644 --- a/src/hooks/useGetTeam.ts +++ b/src/hooks/useGetTeam.ts @@ -2,28 +2,28 @@ import { useQuery } from '@tanstack/react-query'; import { useCallback } from 'react'; import { BackendApiClient } from '@/integrations/new-be/BackendApi'; -import { useCookies } from '@/providers/CookiesProvider'; import { ProjectResponse, TeamResponse } from '@/types/Billing'; import { Log } from '@/utils/log'; +import { useAuthContext } from '@/providers/AuthProvider'; type UseGetTeamArgs = { pause?: boolean; }; export const useGetTeam = ({ pause }: UseGetTeamArgs) => { - const cookies = useCookies(); + const auth = useAuthContext(); const backendApi = new BackendApiClient({ - accessToken: cookies.values.accessToken, + accessToken: auth.token, }); const getTeam = useCallback(async () => { - if (pause) { + if (pause || !auth.tokenProjectId) { return null; } try { const projectResponse = await backendApi.fetch({ - url: `/api/v1/projects/${cookies.values.projectId}`, + url: `/api/v1/projects/${auth.tokenProjectId}`, }); const projectResult: ProjectResponse = await projectResponse.json(); @@ -59,10 +59,10 @@ export const useGetTeam = ({ pause }: UseGetTeamArgs) => { throw error; } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [cookies.values.accessToken, pause]); + }, [auth.token, pause]); return useQuery({ - queryKey: ['team', cookies.values.accessToken], + queryKey: ['team', auth.token], queryFn: getTeam, }); }; diff --git a/src/hooks/useUpdateUserMutation.ts b/src/hooks/useUpdateUserMutation.ts new file mode 100644 index 00000000..30dd24ee --- /dev/null +++ b/src/hooks/useUpdateUserMutation.ts @@ -0,0 +1,22 @@ +import { useMutation } from '@tanstack/react-query'; + +import { + UpdateUserDocument, + UpdateUserMutation, + UpdateUserMutationVariables, +} from '@/generated/graphqlClient'; +import { GraphqlApiClient } from '@/integrations/graphql/GraphqlApi'; + +import { useAuthCookie } from './useAuthCookie'; + +export const useUpdateUserMutation = () => { + const [accessToken] = useAuthCookie(); + const graphqlApi = new GraphqlApiClient({ accessToken }); + + return useMutation(async (data: UpdateUserMutationVariables) => { + return graphqlApi.fetch({ + document: UpdateUserDocument.loc.source.body, + variables: data, + }); + }); +}; diff --git a/src/integrations/graphql/GraphqlApi.ts b/src/integrations/graphql/GraphqlApi.ts new file mode 100644 index 00000000..b508addd --- /dev/null +++ b/src/integrations/graphql/GraphqlApi.ts @@ -0,0 +1,83 @@ +import { constants } from '@fleek-platform/utils-permissions'; + +import { secrets } from '@/secrets'; + +export class GraphqlApiClient { + private baseURL: string | undefined = + secrets.NEXT_PUBLIC_SDK__AUTHENTICATION_URL; + private headers: HeadersInit; + + constructor(args: GraphqlApiClient.Arguments) { + this.headers = { + 'Content-Type': 'application/json', + [constants.AUTHORIZATION_HEADER_NAME]: `Bearer ${args.accessToken}`, + [constants.CUSTOM_HEADERS.clientType]: constants.UI_CLIENT_TYPE_NAME, + }; + } + + async fetch( + args: GraphqlApiClient.FetchProps, + ): Promise { + const { document, variables, extraHeaders } = args; + + const response = await fetch(this.baseURL!, { + method: 'POST', + headers: { + ...this.headers, + ...extraHeaders, + }, + body: JSON.stringify({ query: document, variables }), + }); + + const json = await response.json(); + + // Handle HTTP errors + if (!response.ok) { + throw new GraphqlApiClient.NetworkError( + response.status, + json.message || response.statusText, + ); + } + + // Handle GraphQL errors + if (json.errors) { + throw new GraphqlApiClient.GraphqlError(json.errors); + } + + return json.data; + } +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace GraphqlApiClient { + export type Arguments = { accessToken?: string }; + + export type FetchProps = { + document: unknown | string; + variables?: TVariables; + extraHeaders?: HeadersInit; + }; + + export class NetworkError extends Error { + constructor( + public status: number, + public message: string, + ) { + super(message); + this.name = 'NetworkError'; + } + } + + export class GraphqlError extends Error { + constructor( + public errors: Array<{ + message: string; + locations?: unknown; + path?: unknown; + }>, + ) { + super(errors.map((e) => e.message).join(', ')); + this.name = 'GraphqlError'; + } + } +} diff --git a/src/integrations/urql.tsx b/src/integrations/urql.tsx index f6bf619e..fb313787 100644 --- a/src/integrations/urql.tsx +++ b/src/integrations/urql.tsx @@ -124,14 +124,6 @@ const addAuthToOperation = ({ }); }; -const getAuth = async ({ token }: { token?: string }) => { - if (token) { - return { token }; - } - - return { token: null }; -}; - // TODO: Logout user on token expiration export const createUrqlClient = ({ token, @@ -1036,7 +1028,7 @@ export const createUrqlClient = ({ }, }), authExchange(async () => { - const authState: AuthState = await getAuth({ token }); + const authState: AuthState = { token: token ?? null }; return { addAuthToOperation: (operation: Operation) => diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 262bf578..b7b4b263 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -3,13 +3,15 @@ import { useEffect } from 'react'; import { Home } from '@/fragments/Home/Home'; import { useSessionContext } from '@/providers/SessionProvider'; import { Page } from '@/types/App'; +import { routes } from '@fleek-platform/utils-routes'; +import { constants } from '@/constants'; const HomePage: Page = () => { const session = useSessionContext(); const handleLogIn = () => { if (!session.error && !session.loading && !session.auth.token) { - session.auth.login('dynamic'); + session.auth.login(routes.project.home({ projectId: '[projectId]' })); } }; diff --git a/src/pages/login/[verificationSessionId].tsx b/src/pages/login/[verificationSessionId].tsx index 0564b4ad..90a8c004 100644 --- a/src/pages/login/[verificationSessionId].tsx +++ b/src/pages/login/[verificationSessionId].tsx @@ -106,7 +106,11 @@ const LoginWithSessionPage: Page = () => { withExternalLink >