mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-28 01:09:11 +03:00
Create Profile Hooks to fetch current user
This commit is contained in:
parent
0eef9871f0
commit
fcdc9aaec4
11
front/package-lock.json
generated
11
front/package-lock.json
generated
@ -20,6 +20,7 @@
|
|||||||
"@types/react": "^18.0.25",
|
"@types/react": "^18.0.25",
|
||||||
"@types/react-dom": "^18.0.9",
|
"@types/react-dom": "^18.0.9",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.4.4",
|
"react-router-dom": "^6.4.4",
|
||||||
@ -25034,6 +25035,11 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jwt-decode": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||||
|
},
|
||||||
"node_modules/kind-of": {
|
"node_modules/kind-of": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
@ -53365,6 +53371,11 @@
|
|||||||
"integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==",
|
"integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"jwt-decode": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="
|
||||||
|
},
|
||||||
"kind-of": {
|
"kind-of": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"@types/react": "^18.0.25",
|
"@types/react": "^18.0.25",
|
||||||
"@types/react-dom": "^18.0.9",
|
"@types/react-dom": "^18.0.9",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.4.4",
|
"react-router-dom": "^6.4.4",
|
||||||
|
@ -6,28 +6,10 @@ import AuthCallback from './pages/AuthCallback';
|
|||||||
import AppLayout from './layout/AppLayout';
|
import AppLayout from './layout/AppLayout';
|
||||||
import RequireAuth from './components/RequireAuth';
|
import RequireAuth from './components/RequireAuth';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router-dom';
|
||||||
import { useQuery, gql } from '@apollo/client';
|
import { useGetProfile } from './hooks/profile/useGetProfile';
|
||||||
|
|
||||||
const GET_USER_PROFILE = gql`
|
|
||||||
query GetUserProfile {
|
|
||||||
users {
|
|
||||||
id
|
|
||||||
email
|
|
||||||
first_name
|
|
||||||
last_name
|
|
||||||
tenant {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const { data } = useQuery(GET_USER_PROFILE, {
|
const { user } = useGetProfile();
|
||||||
fetchPolicy: 'network-only',
|
|
||||||
});
|
|
||||||
const user = data?.users[0];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout user={user}>
|
<AppLayout user={user}>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import AuthService from '../hooks/AuthenticationHooks';
|
import { redirectIfNotLoggedIn } from '../hooks/AuthenticationHooks';
|
||||||
|
|
||||||
function RequireAuth({ children }: { children: JSX.Element }): JSX.Element {
|
function RequireAuth({ children }: { children: JSX.Element }): JSX.Element {
|
||||||
const { redirectIfNotLoggedIn } = AuthService;
|
|
||||||
redirectIfNotLoggedIn();
|
redirectIfNotLoggedIn();
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { useAuth0 } from '@auth0/auth0-react';
|
import { useAuth0 } from '@auth0/auth0-react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import jwt from 'jwt-decode';
|
||||||
|
import { TokenPayload } from '../interfaces/TokenPayload.interface';
|
||||||
|
|
||||||
const useIsNotLoggedIn = () => {
|
const useIsNotLoggedIn = () => {
|
||||||
const { isAuthenticated, isLoading } = useAuth0();
|
const { isAuthenticated, isLoading } = useAuth0();
|
||||||
@ -15,6 +17,17 @@ const redirectIfNotLoggedIn = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useGetUserEmailFromToken = (): string | undefined => {
|
||||||
|
const token = localStorage.getItem('accessToken');
|
||||||
|
|
||||||
|
const payload: TokenPayload | undefined = token ? jwt(token) : undefined;
|
||||||
|
if (!payload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload['https://hasura.io/jwt/claims']['x-hasura-user-email'];
|
||||||
|
};
|
||||||
|
|
||||||
const useGetAccessToken = () => {
|
const useGetAccessToken = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [token, setToken] = useState('');
|
const [token, setToken] = useState('');
|
||||||
@ -36,4 +49,9 @@ const useGetAccessToken = () => {
|
|||||||
return { loading, token };
|
return { loading, token };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { useIsNotLoggedIn, useGetAccessToken, redirectIfNotLoggedIn };
|
export {
|
||||||
|
useIsNotLoggedIn,
|
||||||
|
useGetAccessToken,
|
||||||
|
redirectIfNotLoggedIn,
|
||||||
|
useGetUserEmailFromToken,
|
||||||
|
};
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import AuthenticationHooks from '../AuthenticationHooks';
|
import {
|
||||||
|
useIsNotLoggedIn,
|
||||||
|
useGetUserEmailFromToken,
|
||||||
|
} from '../AuthenticationHooks';
|
||||||
import { useAuth0 } from '@auth0/auth0-react';
|
import { useAuth0 } from '@auth0/auth0-react';
|
||||||
import { mocked } from 'jest-mock';
|
import { mocked } from 'jest-mock';
|
||||||
|
|
||||||
@ -32,7 +35,6 @@ describe('useIsNotLoggedIn', () => {
|
|||||||
isLoading: true,
|
isLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { useIsNotLoggedIn } = AuthenticationHooks;
|
|
||||||
const { result } = renderHook(() => useIsNotLoggedIn());
|
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||||
const isNotLoggedIn = result.current;
|
const isNotLoggedIn = result.current;
|
||||||
|
|
||||||
@ -53,7 +55,6 @@ describe('useIsNotLoggedIn', () => {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { useIsNotLoggedIn } = AuthenticationHooks;
|
|
||||||
const { result } = renderHook(() => useIsNotLoggedIn());
|
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||||
const isNotLoggedIn = result.current;
|
const isNotLoggedIn = result.current;
|
||||||
|
|
||||||
@ -75,7 +76,6 @@ describe('useIsNotLoggedIn', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.localStorage.setItem('accessToken', 'token');
|
window.localStorage.setItem('accessToken', 'token');
|
||||||
const { useIsNotLoggedIn } = AuthenticationHooks;
|
|
||||||
const { result } = renderHook(() => useIsNotLoggedIn());
|
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||||
const isNotLoggedIn = result.current;
|
const isNotLoggedIn = result.current;
|
||||||
|
|
||||||
@ -97,10 +97,33 @@ describe('useIsNotLoggedIn', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.localStorage.setItem('accessToken', 'token');
|
window.localStorage.setItem('accessToken', 'token');
|
||||||
const { useIsNotLoggedIn } = AuthenticationHooks;
|
|
||||||
const { result } = renderHook(() => useIsNotLoggedIn());
|
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||||
const isNotLoggedIn = result.current;
|
const isNotLoggedIn = result.current;
|
||||||
|
|
||||||
expect(isNotLoggedIn).toBe(false);
|
expect(isNotLoggedIn).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('useGetUserEmailFromToken', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
window.localStorage.clear();
|
||||||
|
jest.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined if token is not there', () => {
|
||||||
|
const { result } = renderHook(() => useGetUserEmailFromToken());
|
||||||
|
const email = result.current;
|
||||||
|
|
||||||
|
expect(email).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns email if token is there', () => {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
'accessToken',
|
||||||
|
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1FQXZiR0dFNjJ4S25mTFNxNHQ0dCJ9.eyJodHRwczovL2hhc3VyYS5pby9qd3QvY2xhaW1zIjp7IngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsidXNlciJdLCJ4LWhhc3VyYS1kZWZhdWx0LXJvbGUiOiJ1c2VyIiwieC1oYXN1cmEtdXNlci1lbWFpbCI6ImNoYXJsZXNAb3VpaGVscC50d2VudHkuY29tIiwieC1oYXN1cmEtdXNlci1pZCI6Imdvb2dsZS1vYXV0aDJ8MTE4MjM1ODk3NDQ2OTIwNTQ3NzMzIn0sImlzcyI6Imh0dHBzOi8vdHdlbnR5LWRldi5ldS5hdXRoMC5jb20vIiwic3ViIjoiZ29vZ2xlLW9hdXRoMnwxMTgyMzU4OTc0NDY5MjA1NDc3MzMiLCJhdWQiOlsiaGFzdXJhLWRldiIsImh0dHBzOi8vdHdlbnR5LWRldi5ldS5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNjc1MzYyNzY0LCJleHAiOjE2NzU0NDkxNjQsImF6cCI6IlM2ZXoyUFdUdUFsRncydjdxTFBWb2hmVXRseHc4QlBhIiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCJ9.DseeSqYzNlYVQfuicoK8fK1Z6b-TYNvCkRoXXYOhg1X3HDSejowUTudyrJGErkT65xMCfx8K5quof9eV8BZQixCPr670r5gAIHxHuGY_KNfHTOALe8E5VyQaoekRyDr99Qo3QxliOOlJxtmckA8FTeD6JanfVmcrqghUOIsSXXDOOzJV6eME7JErEjTQHpfxveSVbPlCmIqZ3fqDaFdKfAlUDZFhVQM8XbfubNmG4VcoMyB7H47yLdGkYvVfPO1lVg0efywQo4IfbtiqFv5CjOEqO6PG78Wfkd24bcilkf6ZuGXsA-w-0xlU089GhKF99lNI1PxvNWAaLFbqanxiEw',
|
||||||
|
);
|
||||||
|
const { result } = renderHook(() => useGetUserEmailFromToken());
|
||||||
|
|
||||||
|
expect(result.current).toBe('charles@ouihelp.twenty.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
25
front/src/hooks/profile/__tests__/useGetProfile.test.tsx
Normal file
25
front/src/hooks/profile/__tests__/useGetProfile.test.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useQuery, QueryResult } from '@apollo/client';
|
||||||
|
import { useGetProfile } from '../useGetProfile';
|
||||||
|
|
||||||
|
jest.mock('@apollo/client', () => ({
|
||||||
|
useQuery: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useGetUserEmailFromToken', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const result: Partial<QueryResult<any>> = {
|
||||||
|
data: { users: [{ email: 'test@twenty.com' }] },
|
||||||
|
loading: false,
|
||||||
|
error: undefined,
|
||||||
|
};
|
||||||
|
(useQuery as jest.Mock).mockImplementation(() => result as QueryResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns profile', () => {
|
||||||
|
const { result } = renderHook(() => useGetProfile());
|
||||||
|
const email = result.current.user?.email;
|
||||||
|
expect(email).toEqual(result.current.user?.email);
|
||||||
|
expect(useQuery).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
33
front/src/hooks/profile/useGetProfile.tsx
Normal file
33
front/src/hooks/profile/useGetProfile.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { ApolloError, useQuery } from '@apollo/client';
|
||||||
|
import { gql } from 'graphql-tag';
|
||||||
|
import { User } from '../../interfaces/user.interface';
|
||||||
|
import { useGetUserEmailFromToken } from '../AuthenticationHooks';
|
||||||
|
|
||||||
|
const GET_USER_PROFILE = gql`
|
||||||
|
query GetUserProfile($email: String!) {
|
||||||
|
users(where: { email: { _eq: $email } }, limit: 1) {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
first_name
|
||||||
|
last_name
|
||||||
|
tenant {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ProfileResult = {
|
||||||
|
loading: boolean;
|
||||||
|
error?: ApolloError;
|
||||||
|
user?: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetProfile = (): ProfileResult => {
|
||||||
|
const email = useGetUserEmailFromToken();
|
||||||
|
const { loading, error, data } = useQuery(GET_USER_PROFILE, {
|
||||||
|
variables: { email },
|
||||||
|
});
|
||||||
|
return { loading, error, user: data?.users[0] };
|
||||||
|
};
|
6
front/src/interfaces/TokenPayload.interface.ts
Normal file
6
front/src/interfaces/TokenPayload.interface.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface TokenPayload {
|
||||||
|
'https://hasura.io/jwt/claims': {
|
||||||
|
'x-hasura-user-email': string;
|
||||||
|
'x-hasura-user-id': string;
|
||||||
|
};
|
||||||
|
}
|
4
front/src/interfaces/tenant.interface.ts
Normal file
4
front/src/interfaces/tenant.interface.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface Tenant {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
9
front/src/interfaces/user.interface.ts
Normal file
9
front/src/interfaces/user.interface.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Tenant } from './tenant.interface';
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
tenant?: Tenant;
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import Navbar from './navbar/Navbar';
|
import Navbar from './navbar/Navbar';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { User } from '../interfaces/user.interface';
|
||||||
|
|
||||||
const StyledLayout = styled.div`
|
const StyledLayout = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -9,12 +10,7 @@ const StyledLayout = styled.div`
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
user?: {
|
user?: User;
|
||||||
email: string;
|
|
||||||
first_name: string;
|
|
||||||
last_name: string;
|
|
||||||
tenant: { id: string; name: string };
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function AppLayout({ children, user }: OwnProps) {
|
function AppLayout({ children, user }: OwnProps) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useMatch, useResolvedPath } from 'react-router-dom';
|
import { useMatch, useResolvedPath } from 'react-router-dom';
|
||||||
|
import { User } from '../../interfaces/user.interface';
|
||||||
import NavItem from './NavItem';
|
import NavItem from './NavItem';
|
||||||
import ProfileContainer from './ProfileContainer';
|
import ProfileContainer from './ProfileContainer';
|
||||||
|
|
||||||
@ -18,12 +19,7 @@ const NavItemsContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
user?: {
|
user?: User;
|
||||||
email: string;
|
|
||||||
first_name: string;
|
|
||||||
last_name: string;
|
|
||||||
tenant: { id: string; name: string };
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function Navbar({ user }: OwnProps) {
|
function Navbar({ user }: OwnProps) {
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { User } from '../../interfaces/user.interface';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
user?: {
|
user?: User;
|
||||||
email: string;
|
|
||||||
first_name: string;
|
|
||||||
last_name: string;
|
|
||||||
tenant: { id: string; name: string };
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.button`
|
const StyledContainer = styled.button`
|
||||||
@ -65,7 +61,7 @@ function ProfileContainer({ user }: OwnProps) {
|
|||||||
</StyledAvatar>
|
</StyledAvatar>
|
||||||
<StyledInfoContainer>
|
<StyledInfoContainer>
|
||||||
<StyledEmail>{user?.email}</StyledEmail>
|
<StyledEmail>{user?.email}</StyledEmail>
|
||||||
<StyledTenant>{user?.tenant.name}</StyledTenant>
|
<StyledTenant>{user?.tenant?.name}</StyledTenant>
|
||||||
</StyledInfoContainer>
|
</StyledInfoContainer>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
@ -11,10 +11,11 @@ export const NavbarOnInsights = () => (
|
|||||||
<MemoryRouter initialEntries={['/insights']}>
|
<MemoryRouter initialEntries={['/insights']}>
|
||||||
<Navbar
|
<Navbar
|
||||||
user={{
|
user={{
|
||||||
|
id: 1,
|
||||||
email: 'charles@twenty.com',
|
email: 'charles@twenty.com',
|
||||||
first_name: 'Charles',
|
first_name: 'Charles',
|
||||||
last_name: 'Bochet',
|
last_name: 'Bochet',
|
||||||
tenant: { id: '1', name: 'Twenty' },
|
tenant: { id: 1, name: 'Twenty' },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import AuthService from '../hooks/AuthenticationHooks';
|
import { useGetAccessToken } from '../hooks/AuthenticationHooks';
|
||||||
|
|
||||||
function AuthCallback() {
|
function AuthCallback() {
|
||||||
const { useGetAccessToken } = AuthService;
|
|
||||||
|
|
||||||
const { token } = useGetAccessToken();
|
const { token } = useGetAccessToken();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -27,7 +27,7 @@ api-make-metadata: ##
|
|||||||
hasura metadata export"
|
hasura metadata export"
|
||||||
|
|
||||||
front-sh: ##
|
front-sh: ##
|
||||||
@docker-compose exec twenty-front bash
|
@docker-compose exec twenty-front sh
|
||||||
|
|
||||||
front-test: ##
|
front-test: ##
|
||||||
@docker-compose exec twenty-front sh -c "npm run test"
|
@docker-compose exec twenty-front sh -c "npm run test"
|
||||||
|
Loading…
Reference in New Issue
Block a user