Separate auth0 users depending on tenants

This commit is contained in:
Charles Bochet
2023-02-03 19:59:00 +01:00
parent d94ed13f4e
commit d58af82c51
13 changed files with 147 additions and 56 deletions

View File

@ -110,7 +110,10 @@ workflows:
deploy-api: deploy-api:
jobs: jobs:
- deploy-api-canary - deploy-api-canary:
filters:
branches:
only: main
- slack/on-hold: - slack/on-hold:
name: slack-notification name: slack-notification
context: slack-secrets context: slack-secrets

View File

@ -10,13 +10,21 @@ array_relationships:
name: users name: users
schema: public schema: public
select_permissions: select_permissions:
- role: public
permission:
columns:
- auth0_client_id
- domain
filter: {}
limit: 1
- role: user - role: user
permission: permission:
columns: columns:
- auth0_client_id
- email_domain
- id - id
- name - name
- uuid - uuid
- email_domain
filter: filter:
users: users:
email: email:

View File

@ -0,0 +1 @@
alter table "public"."tenants" drop column "auth0_client_id";

View File

@ -0,0 +1 @@
alter table "public"."tenants" add column "auth0_client_id" text null;

View File

@ -0,0 +1 @@
alter table "public"."tenants" drop column "domain";

View File

@ -0,0 +1,2 @@
alter table "public"."tenants" add column "domain" text
null default 'pilot.twenty.com';

View File

@ -1,4 +1,5 @@
import React from 'react'; import { Auth0Provider } from '@auth0/auth0-react';
import React, { useEffect } 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';
@ -7,40 +8,59 @@ 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 { useGetProfile } from './hooks/profile/useGetProfile'; import { useGetProfile } from './hooks/profile/useGetProfile';
import { useGetTenantByDomain } from './hooks/tenant/useGetTenantByDomain';
function App() { function App() {
const { tenant } = useGetTenantByDomain();
const { user } = useGetProfile(); const { user } = useGetProfile();
return ( return (
<AppLayout user={user}> <div>
<Routes> {tenant && (
<Route <Auth0Provider
path="/" domain={process.env.REACT_APP_AUTH0_DOMAIN || ''}
element={ clientId={tenant?.auth0_client_id || ''}
<RequireAuth> authorizationParams={{
<Inbox /> redirect_uri:
</RequireAuth> window.location.protocol +
} '//' +
/> window.location.host +
<Route process.env.REACT_APP_AUTH0_CALLBACK_URL || '',
path="/contacts" audience: process.env.REACT_APP_AUTH0_AUDIENCE || '',
element={ }}
<RequireAuth> >
<Contacts /> <AppLayout user={user}>
</RequireAuth> <Routes>
} <Route
/> path="/"
<Route element={
path="/insights" <RequireAuth>
element={ <Inbox />
<RequireAuth> </RequireAuth>
<Insights /> }
</RequireAuth> />
} <Route
/> path="/contacts"
<Route path="/auth/callback" element={<AuthCallback />} /> element={
</Routes> <RequireAuth>
</AppLayout> <Contacts />
</RequireAuth>
}
/>
<Route
path="/insights"
element={
<RequireAuth>
<Insights />
</RequireAuth>
}
/>
<Route path="/auth/callback" element={<AuthCallback />} />
</Routes>
</AppLayout>
</Auth0Provider>
)}
</div>
); );
} }

View File

@ -0,0 +1,25 @@
import { renderHook } from '@testing-library/react';
import { useQuery, QueryResult } from '@apollo/client';
import { useGetTenantByDomain } from '../useGetTenantByDomain';
jest.mock('@apollo/client', () => ({
useQuery: jest.fn(),
}));
describe('useGetTenantByDomain', () => {
beforeEach(() => {
const result: Partial<QueryResult<any>> = {
data: { tenants: [{ domain: 'pilot.twenty.com' }] },
loading: false,
error: undefined,
};
(useQuery as jest.Mock).mockImplementation(() => result as QueryResult);
});
it('returns tenant by domain', () => {
const { result } = renderHook(() => useGetTenantByDomain());
const domain = result.current.tenant?.domain;
expect(domain).toEqual(result.current.tenant?.domain);
expect(useQuery).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,31 @@
import { ApolloError, useQuery } from '@apollo/client';
import { gql } from 'graphql-tag';
import { Tenant } from '../../interfaces/tenant.interface';
const GET_TENANT_BY_DOMAIN = gql`
query GetTenantByDomain($domain: String!) {
tenants(where: { domain: { _eq: $domain } }, limit: 1) {
auth0_client_id
domain
}
}
`;
type TenantResult = {
loading: boolean;
error?: ApolloError;
tenant?: Tenant;
};
export const useGetTenantByDomain = (): TenantResult => {
const domain = window.location.hostname;
const { loading, error, data } = useQuery(GET_TENANT_BY_DOMAIN, {
variables: { domain },
context: {
headers: {
'x-hasura-default-role': 'public',
},
},
});
return { loading, error, tenant: data?.tenants[0] };
};

View File

@ -3,7 +3,6 @@ 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 { import {
ApolloClient, ApolloClient,
InMemoryCache, InMemoryCache,
@ -15,12 +14,17 @@ import { setContext } from '@apollo/client/link/context';
const httpLink = createHttpLink({ uri: process.env.REACT_APP_API_URL }); const httpLink = createHttpLink({ uri: process.env.REACT_APP_API_URL });
const authLink = setContext((_, { headers }) => { const authLink = setContext((_, { headers }) => {
const requestHeaders = { ...headers };
const token = localStorage.getItem('accessToken'); const token = localStorage.getItem('accessToken');
const headerContainsPublicRole =
requestHeaders.hasOwnProperty('x-hasura-default-role') &&
requestHeaders['x-hasura-default-role'] === 'public';
if (!headerContainsPublicRole && token) {
requestHeaders['authorization'] = `Bearer ${token}`;
}
return { return {
headers: { headers: requestHeaders,
...headers,
authorization: token ? `Bearer ${token}` : '',
},
}; };
}); });
@ -33,22 +37,9 @@ const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement, document.getElementById('root') as HTMLElement,
); );
root.render( root.render(
<Auth0Provider <ApolloProvider client={client}>
domain={process.env.REACT_APP_AUTH0_DOMAIN || ''} <BrowserRouter>
clientId={process.env.REACT_APP_AUTH0_CLIENT_ID || ''} <App />
authorizationParams={{ </BrowserRouter>
redirect_uri: </ApolloProvider>,
window.location.protocol +
'//' +
window.location.host +
process.env.REACT_APP_AUTH0_CALLBACK_URL || '',
audience: process.env.REACT_APP_AUTH0_AUDIENCE || '',
}}
>
<ApolloProvider client={client}>
<BrowserRouter>
<App />
</BrowserRouter>
</ApolloProvider>
</Auth0Provider>,
); );

View File

@ -1,4 +1,6 @@
export interface Tenant { export interface Tenant {
id: number; id: number;
name: string; name: string;
domain: string;
auth0_client_id: string;
} }

View File

@ -15,7 +15,12 @@ export const NavbarOnInsights = () => (
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',
domain: 'pilot.twenty.com',
auth0_client_id: 'auth0_client_id',
},
}} }}
/> />
</MemoryRouter> </MemoryRouter>

View File

@ -36,6 +36,7 @@ services:
HASURA_GRAPHQL_PG_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/twenty HASURA_GRAPHQL_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_UNAUTHORIZED_ROLE: "public"
HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET} HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET}
HASURA_GRAPHQL_JWT_SECRET: ${HASURA_GRAPHQL_JWT_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