mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-22 19:41:53 +03:00
Generate Token through Auth0
This commit is contained in:
parent
54acb16db8
commit
8e0dc44bf6
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
.vscode/*
|
.vscode/*
|
||||||
|
**/**/.env
|
97
README.md
97
README.md
@ -1,40 +1,87 @@
|
|||||||
# twenty
|
# Twenty
|
||||||
|
|
||||||
Welcome to Twenty!
|
Welcome to Twenty documentation!
|
||||||
|
|
||||||
## Setup & Development
|
## High Level Overview
|
||||||
|
|
||||||
|
Twenty development stack is composed of 5 different layers:
|
||||||
|
- twenty-front: our frontend React app
|
||||||
|
- twenty-api (Hasura): our backend presentation layer that can do straight forward CRUDs, permissionning, authentication.
|
||||||
|
- twenty-server: our backend that contain complex logics, scripts, jobs...
|
||||||
|
- [tbd] twenty-events (Jitsu): our event ingestor which is separated from api and server to ensure high availability
|
||||||
|
- storages: postgres, [tbd] elasticsearch, [tbd] redis.
|
||||||
|
|
||||||
|
## Development environment setup
|
||||||
|
|
||||||
|
This section only discusses the development setup. The whole developemnt environment is containerized with Docker and orchestrated with docker-compose.
|
||||||
|
|
||||||
|
### Step 1: pre-requesites
|
||||||
|
Make sure to have the latest Docker and Docker-compose versions installed on your computer.
|
||||||
|
|
||||||
|
### Step 2: docker build
|
||||||
|
Build docker containers.
|
||||||
|
|
||||||
|
The whole setup/development experience is happening in `infra/dev` folder. Make sure to be in this folder:
|
||||||
```
|
```
|
||||||
docker-compose -f infra/dev/docker-compose.yml up --build --force-recreate
|
cd infra/dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Browse:
|
|
||||||
- FE/BE: localhost:3000
|
|
||||||
- Hasura: localhost:8080
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
Ssh into the twenty-server container using:
|
|
||||||
- `docker ps` to get the container id
|
|
||||||
- `docker exec -it CONTAINER_ID sh`
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cd front
|
docker-compose up --build --force-recreate
|
||||||
npm run test
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backend
|
Once this is completed you should have:
|
||||||
|
- twenty-front available on: http://localhost:3001
|
||||||
|
- twenty-api available on: http://localhost:8080
|
||||||
|
- twenty-server available on: http://localhost:3000/health
|
||||||
|
- postgres: available on http://localhost:5432 that should contain two database: twenty (data) and hasura (metadata)
|
||||||
|
|
||||||
|
### Step 3: environment file
|
||||||
|
Configure your environment by copying the `.env.example` file located in `infra/dev` folder into `.env`.
|
||||||
```
|
```
|
||||||
cd server
|
cp infra/dev/.env.example infra/dev/.env
|
||||||
npm run test
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Production
|
Then, you'll need to replace all REPLACE_ME variable by their development value. Please reach out to another engineer to get these values (as most of them are third party credentials, sensitive data)
|
||||||
|
|
||||||
|
### Step 4: API (Hasura) metadata
|
||||||
|
Browse Hasura console on http://localhost:8080, go to settings and import metadata file located in `infra/dev/twenty-api` folder
|
||||||
|
|
||||||
|
## Developping on Frontend
|
||||||
|
|
||||||
|
The development FE server is running on docker up and is exposing the `twenty-front` on port http://localhost:3001. As you modify the `/front` folder on your computer, this folder is synced with your `twenty-front` container and the frontend application is automatically refreshed.
|
||||||
|
|
||||||
|
### Develop
|
||||||
|
|
||||||
|
Recommended: as you modify frontend code, here is how to access `twenty-front` server logs in order to debug / watch typescript issues:
|
||||||
```
|
```
|
||||||
cd front && npm run build
|
docker-compose up
|
||||||
cd ../server && npm run build
|
docker-compose logs twenty-front -f
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Open a shell into the container
|
||||||
|
```
|
||||||
|
docker-compose exec twenty-front sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
#### Unit tests:
|
||||||
|
```
|
||||||
|
docker-compose exec twenty-front sh -c "npm run test"
|
||||||
|
# coverage
|
||||||
|
docker-compose exec twenty-front sh -c "npm run coverage"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Storybook:
|
||||||
|
```
|
||||||
|
docker-compose exec twenty-front sh -c "npm run storybook"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developping on API
|
||||||
|
|
||||||
|
The API is a Hasura instance which is a no-code container. To modify API behavior, you'll need to connect to Hasura console on: http://localhost:8080/console
|
||||||
|
|
||||||
|
## Developping on server
|
||||||
|
|
||||||
|
Section TBD
|
5350
front/package-lock.json
generated
5350
front/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@apollo/client": "^3.7.5",
|
||||||
|
"@auth0/auth0-react": "^2.0.0",
|
||||||
"@emotion/react": "^11.10.5",
|
"@emotion/react": "^11.10.5",
|
||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||||
@ -12,6 +14,7 @@
|
|||||||
"@types/node": "^16.18.4",
|
"@types/node": "^16.18.4",
|
||||||
"@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",
|
||||||
"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",
|
||||||
@ -19,7 +22,7 @@
|
|||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "cp ../infra/dev/.env ./.env && PORT=3001 react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
@ -46,8 +49,7 @@
|
|||||||
"overrides": {
|
"overrides": {
|
||||||
"react-refresh": "0.14.0"
|
"react-refresh": "0.14.0"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {},
|
||||||
},
|
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
|
@ -2,16 +2,61 @@ import React from 'react';
|
|||||||
import Inbox from './pages/inbox/Inbox';
|
import Inbox from './pages/inbox/Inbox';
|
||||||
import Contacts from './pages/Contacts';
|
import Contacts from './pages/Contacts';
|
||||||
import Insights from './pages/Insights';
|
import Insights from './pages/Insights';
|
||||||
|
import AuthCallback from './pages/AuthCallback';
|
||||||
import AppLayout from './layout/AppLayout';
|
import AppLayout from './layout/AppLayout';
|
||||||
|
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';
|
||||||
|
|
||||||
|
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, {
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
});
|
||||||
|
const user = data?.users[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout user={user}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Inbox />} />
|
<Route
|
||||||
<Route path="/contacts" element={<Contacts />} />
|
path="/"
|
||||||
<Route path="/insights" element={<Insights />} />
|
element={
|
||||||
|
<RequireAuth>
|
||||||
|
<Inbox />
|
||||||
|
</RequireAuth>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/contacts"
|
||||||
|
element={
|
||||||
|
<RequireAuth>
|
||||||
|
<Contacts />
|
||||||
|
</RequireAuth>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/insights"
|
||||||
|
element={
|
||||||
|
<RequireAuth>
|
||||||
|
<Insights />
|
||||||
|
</RequireAuth>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
|
10
front/src/components/RequireAuth.tsx
Normal file
10
front/src/components/RequireAuth.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import AuthService from '../hooks/AuthenticationHooks';
|
||||||
|
|
||||||
|
function RequireAuth({ children }: { children: JSX.Element }): JSX.Element {
|
||||||
|
const { redirectIfNotLoggedIn } = AuthService;
|
||||||
|
redirectIfNotLoggedIn();
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RequireAuth;
|
14
front/src/components/__stories__/RequireAuth.stories.tsx
Normal file
14
front/src/components/__stories__/RequireAuth.stories.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import RequireAuth from '../RequireAuth';
|
||||||
|
|
||||||
|
import Navbar from '../RequireAuth';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'RequireAuth',
|
||||||
|
component: Navbar,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RequireAuthWithHelloChild = () => (
|
||||||
|
<RequireAuth>
|
||||||
|
<div>Hello</div>
|
||||||
|
</RequireAuth>
|
||||||
|
);
|
9
front/src/components/__tests__/RequireAuth.test.tsx
Normal file
9
front/src/components/__tests__/RequireAuth.test.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { RequireAuthWithHelloChild } from '../__stories__/RequireAuth.stories';
|
||||||
|
|
||||||
|
it('Checks the Require Auth renders', () => {
|
||||||
|
const { getAllByText } = render(<RequireAuthWithHelloChild />);
|
||||||
|
|
||||||
|
expect(getAllByText('Hello')).toBeTruthy();
|
||||||
|
});
|
39
front/src/hooks/AuthenticationHooks.ts
Normal file
39
front/src/hooks/AuthenticationHooks.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { useAuth0 } from '@auth0/auth0-react';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
const useIsNotLoggedIn = () => {
|
||||||
|
const { isAuthenticated, isLoading } = useAuth0();
|
||||||
|
const hasAccessToken = localStorage.getItem('accessToken');
|
||||||
|
return (!isAuthenticated || !hasAccessToken) && !isLoading;
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirectIfNotLoggedIn = () => {
|
||||||
|
const isNotLoggedIn = useIsNotLoggedIn();
|
||||||
|
const { loginWithRedirect } = useAuth0();
|
||||||
|
if (isNotLoggedIn) {
|
||||||
|
loginWithRedirect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const useGetAccessToken = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [token, setToken] = useState('');
|
||||||
|
const { getAccessTokenSilently } = useAuth0();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchToken = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const accessToken = await getAccessTokenSilently();
|
||||||
|
localStorage.setItem('accessToken', accessToken);
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
setToken(accessToken);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchToken();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { loading, token };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { useIsNotLoggedIn, useGetAccessToken, redirectIfNotLoggedIn };
|
106
front/src/hooks/__tests__/AuthenticationHooks.test.tsx
Normal file
106
front/src/hooks/__tests__/AuthenticationHooks.test.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import AuthenticationHooks from '../AuthenticationHooks';
|
||||||
|
import { useAuth0 } from '@auth0/auth0-react';
|
||||||
|
import { mocked } from 'jest-mock';
|
||||||
|
|
||||||
|
jest.mock('@auth0/auth0-react');
|
||||||
|
const mockedUseAuth0 = mocked(useAuth0, true);
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
email: 'johndoe@me.com',
|
||||||
|
email_verified: true,
|
||||||
|
sub: 'google-oauth2|12345678901234',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('useIsNotLoggedIn', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
window.localStorage.clear();
|
||||||
|
jest.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if auth0 is loading', () => {
|
||||||
|
mockedUseAuth0.mockReturnValue({
|
||||||
|
isAuthenticated: false,
|
||||||
|
user,
|
||||||
|
logout: jest.fn(),
|
||||||
|
loginWithRedirect: jest.fn(),
|
||||||
|
getAccessTokenWithPopup: jest.fn(),
|
||||||
|
getAccessTokenSilently: jest.fn(),
|
||||||
|
getIdTokenClaims: jest.fn(),
|
||||||
|
loginWithPopup: jest.fn(),
|
||||||
|
handleRedirectCallback: jest.fn(),
|
||||||
|
isLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { useIsNotLoggedIn } = AuthenticationHooks;
|
||||||
|
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||||
|
const isNotLoggedIn = result.current;
|
||||||
|
|
||||||
|
expect(isNotLoggedIn).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if token is not there', () => {
|
||||||
|
mockedUseAuth0.mockReturnValue({
|
||||||
|
isAuthenticated: false,
|
||||||
|
user,
|
||||||
|
logout: jest.fn(),
|
||||||
|
loginWithRedirect: jest.fn(),
|
||||||
|
getAccessTokenWithPopup: jest.fn(),
|
||||||
|
getAccessTokenSilently: jest.fn(),
|
||||||
|
getIdTokenClaims: jest.fn(),
|
||||||
|
loginWithPopup: jest.fn(),
|
||||||
|
handleRedirectCallback: jest.fn(),
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { useIsNotLoggedIn } = AuthenticationHooks;
|
||||||
|
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||||
|
const isNotLoggedIn = result.current;
|
||||||
|
|
||||||
|
expect(isNotLoggedIn).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if token is there but user is not connected on auth0', () => {
|
||||||
|
mockedUseAuth0.mockReturnValue({
|
||||||
|
isAuthenticated: false,
|
||||||
|
user,
|
||||||
|
logout: jest.fn(),
|
||||||
|
loginWithRedirect: jest.fn(),
|
||||||
|
getAccessTokenWithPopup: jest.fn(),
|
||||||
|
getAccessTokenSilently: jest.fn(),
|
||||||
|
getIdTokenClaims: jest.fn(),
|
||||||
|
loginWithPopup: jest.fn(),
|
||||||
|
handleRedirectCallback: jest.fn(),
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.localStorage.setItem('accessToken', 'token');
|
||||||
|
const { useIsNotLoggedIn } = AuthenticationHooks;
|
||||||
|
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||||
|
const isNotLoggedIn = result.current;
|
||||||
|
|
||||||
|
expect(isNotLoggedIn).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if token is there and user is connected on auth0', () => {
|
||||||
|
mockedUseAuth0.mockReturnValue({
|
||||||
|
isAuthenticated: true,
|
||||||
|
user,
|
||||||
|
logout: jest.fn(),
|
||||||
|
loginWithRedirect: jest.fn(),
|
||||||
|
getAccessTokenWithPopup: jest.fn(),
|
||||||
|
getAccessTokenSilently: jest.fn(),
|
||||||
|
getIdTokenClaims: jest.fn(),
|
||||||
|
loginWithPopup: jest.fn(),
|
||||||
|
handleRedirectCallback: jest.fn(),
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.localStorage.setItem('accessToken', 'token');
|
||||||
|
const { useIsNotLoggedIn } = AuthenticationHooks;
|
||||||
|
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||||
|
const isNotLoggedIn = result.current;
|
||||||
|
|
||||||
|
expect(isNotLoggedIn).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
@ -3,12 +3,48 @@ import ReactDOM from 'react-dom/client';
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { Auth0Provider } from '@auth0/auth0-react';
|
||||||
|
import {
|
||||||
|
ApolloClient,
|
||||||
|
InMemoryCache,
|
||||||
|
ApolloProvider,
|
||||||
|
createHttpLink,
|
||||||
|
} from '@apollo/client';
|
||||||
|
import { setContext } from '@apollo/client/link/context';
|
||||||
|
|
||||||
|
const httpLink = createHttpLink({ uri: process.env.REACT_APP_API_URL });
|
||||||
|
|
||||||
|
const authLink = setContext((_, { headers }) => {
|
||||||
|
const token = localStorage.getItem('accessToken');
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
authorization: token ? `Bearer ${token}` : '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new ApolloClient({
|
||||||
|
link: authLink.concat(httpLink),
|
||||||
|
cache: new InMemoryCache(),
|
||||||
|
});
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement,
|
document.getElementById('root') as HTMLElement,
|
||||||
);
|
);
|
||||||
root.render(
|
root.render(
|
||||||
<BrowserRouter>
|
<Auth0Provider
|
||||||
<App />
|
domain={process.env.REACT_APP_AUTH0_DOMAIN || ''}
|
||||||
</BrowserRouter>,
|
clientId={process.env.REACT_APP_AUTH0_CLIENT_ID || ''}
|
||||||
|
authorizationParams={{
|
||||||
|
redirect_uri: process.env.REACT_APP_AUTH0_CALLBACK_URL || '',
|
||||||
|
audience: process.env.REACT_APP_AUTH0_AUDIENCE || '',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ApolloProvider client={client}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</ApolloProvider>
|
||||||
|
</Auth0Provider>,
|
||||||
);
|
);
|
||||||
|
@ -9,12 +9,18 @@ const StyledLayout = styled.div`
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
|
user?: {
|
||||||
|
email: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
tenant: { id: string; name: string };
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
function AppLayout({ children }: OwnProps) {
|
function AppLayout({ children, user }: OwnProps) {
|
||||||
return (
|
return (
|
||||||
<StyledLayout>
|
<StyledLayout>
|
||||||
<Navbar />
|
<Navbar user={user} />
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</StyledLayout>
|
</StyledLayout>
|
||||||
);
|
);
|
||||||
|
@ -1,50 +1,68 @@
|
|||||||
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 NavItem from './NavItem';
|
import NavItem from './NavItem';
|
||||||
|
import ProfileContainer from './ProfileContainer';
|
||||||
|
|
||||||
const NavbarContainer = styled.div`
|
const NavbarContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: stretch;
|
justify-content: space-between;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
height: 58px;
|
height: 58px;
|
||||||
border-bottom: 2px solid #eaecee;
|
border-bottom: 2px solid #eaecee;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function Navbar() {
|
const NavItemsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
user?: {
|
||||||
|
email: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
tenant: { id: string; name: string };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function Navbar({ user }: OwnProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NavbarContainer>
|
<NavbarContainer>
|
||||||
<NavItem
|
<NavItemsContainer>
|
||||||
label="Inbox"
|
<NavItem
|
||||||
to="/"
|
label="Inbox"
|
||||||
active={
|
to="/"
|
||||||
!!useMatch({
|
active={
|
||||||
path: useResolvedPath('/').pathname,
|
!!useMatch({
|
||||||
end: true,
|
path: useResolvedPath('/').pathname,
|
||||||
})
|
end: true,
|
||||||
}
|
})
|
||||||
/>
|
}
|
||||||
<NavItem
|
/>
|
||||||
label="Contacts"
|
<NavItem
|
||||||
to="/contacts"
|
label="Contacts"
|
||||||
active={
|
to="/contacts"
|
||||||
!!useMatch({
|
active={
|
||||||
path: useResolvedPath('/contacts').pathname,
|
!!useMatch({
|
||||||
end: true,
|
path: useResolvedPath('/contacts').pathname,
|
||||||
})
|
end: true,
|
||||||
}
|
})
|
||||||
/>
|
}
|
||||||
<NavItem
|
/>
|
||||||
label="Insights"
|
<NavItem
|
||||||
to="/insights"
|
label="Insights"
|
||||||
active={
|
to="/insights"
|
||||||
!!useMatch({
|
active={
|
||||||
path: useResolvedPath('/insights').pathname,
|
!!useMatch({
|
||||||
end: true,
|
path: useResolvedPath('/insights').pathname,
|
||||||
})
|
end: true,
|
||||||
}
|
})
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
</NavItemsContainer>
|
||||||
|
<ProfileContainer user={user} />
|
||||||
</NavbarContainer>
|
</NavbarContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
74
front/src/layout/navbar/ProfileContainer.tsx
Normal file
74
front/src/layout/navbar/ProfileContainer.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
user?: {
|
||||||
|
email: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
tenant: { id: string; name: string };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled.button`
|
||||||
|
display: flex;
|
||||||
|
height: 60px;
|
||||||
|
background: inherit;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledInfoContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEmail = styled.div`
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTenant = styled.div`
|
||||||
|
display: flex;
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-weight: bold;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledAvatar = styled.div`
|
||||||
|
display: flex;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 40px;
|
||||||
|
background: black;
|
||||||
|
font-size: 20px;
|
||||||
|
color: white;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function ProfileContainer({ user }: OwnProps) {
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<StyledAvatar>
|
||||||
|
{user?.first_name
|
||||||
|
.split(' ')
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join('')}
|
||||||
|
</StyledAvatar>
|
||||||
|
<StyledInfoContainer>
|
||||||
|
<StyledEmail>{user?.email}</StyledEmail>
|
||||||
|
<StyledTenant>{user?.tenant.name}</StyledTenant>
|
||||||
|
</StyledInfoContainer>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProfileContainer;
|
@ -9,6 +9,13 @@ export default {
|
|||||||
|
|
||||||
export const NavbarOnInsights = () => (
|
export const NavbarOnInsights = () => (
|
||||||
<MemoryRouter initialEntries={['/insights']}>
|
<MemoryRouter initialEntries={['/insights']}>
|
||||||
<Navbar />
|
<Navbar
|
||||||
|
user={{
|
||||||
|
email: 'charles@twenty.com',
|
||||||
|
first_name: 'Charles',
|
||||||
|
last_name: 'Bochet',
|
||||||
|
tenant: { id: '1', name: 'Twenty' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
18
front/src/pages/AuthCallback.tsx
Normal file
18
front/src/pages/AuthCallback.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import AuthService from '../hooks/AuthenticationHooks';
|
||||||
|
|
||||||
|
function AuthCallback() {
|
||||||
|
const { useGetAccessToken } = AuthService;
|
||||||
|
|
||||||
|
const { token } = useGetAccessToken();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (token) {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthCallback;
|
@ -1,5 +1,3 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
function PluginHistory() {
|
function PluginHistory() {
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
}
|
}
|
||||||
|
6
infra/dev/.env.example
Normal file
6
infra/dev/.env.example
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
HASURA_GRAPHQL_JWT_SECRET=REPLACE_ME
|
||||||
|
HASURA_GRAPHQL_ADMIN_SECRET=hasura_secret
|
||||||
|
REACT_APP_AUTH0_DOMAIN=twenty-dev.eu.auth0.com
|
||||||
|
REACT_APP_AUTH0_CLIENT_ID=REPLACE_ME
|
||||||
|
REACT_APP_AUTH0_CALLBACK_URL=http://localhost:3001/auth/callback
|
||||||
|
REACT_APP_AUTH0_AUDIENCE=hasura-dev
|
@ -1,5 +1,17 @@
|
|||||||
version: "3.9"
|
version: "3.9"
|
||||||
services:
|
services:
|
||||||
|
twenty-front:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: ./infra/dev/twenty-front/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3001:3001"
|
||||||
|
- "6006:6006"
|
||||||
|
volumes:
|
||||||
|
- ../../front:/app/front
|
||||||
|
- ../../infra:/app/infra
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
twenty-server:
|
twenty-server:
|
||||||
build:
|
build:
|
||||||
context: ../..
|
context: ../..
|
||||||
@ -20,6 +32,8 @@ services:
|
|||||||
PG_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/twenty
|
PG_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/twenty
|
||||||
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
|
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
|
||||||
HASURA_GRAPHQL_DEV_MODE: "true"
|
HASURA_GRAPHQL_DEV_MODE: "true"
|
||||||
|
HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET}
|
||||||
|
HASURA_GRAPHQL_JWT_SECRET: ${HASURA_GRAPHQL_JWT_SECRET}
|
||||||
HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
|
HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
|
||||||
postgres:
|
postgres:
|
||||||
build: ./postgres
|
build: ./postgres
|
||||||
|
10
infra/dev/twenty-front/Dockerfile
Normal file
10
infra/dev/twenty-front/Dockerfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FROM node:18-alpine as app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY ../.. .
|
||||||
|
|
||||||
|
WORKDIR /app/front
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start"]
|
@ -3,10 +3,6 @@ FROM node:18-alpine as app
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ../.. .
|
COPY ../.. .
|
||||||
|
|
||||||
WORKDIR /app/front
|
|
||||||
RUN npm install
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
WORKDIR /app/server
|
WORKDIR /app/server
|
||||||
RUN npm install
|
RUN npm install
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
Loading…
Reference in New Issue
Block a user