Merge pull request #23 from twentyhq/charles-bochet-separate-tenant-users

Separate auth0 users depending on tenants
This commit is contained in:
Charles Bochet
2023-02-03 20:08:19 +01:00
committed by GitHub
13 changed files with 147 additions and 56 deletions

View File

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

View File

@ -10,13 +10,21 @@ array_relationships:
name: users
schema: public
select_permissions:
- role: public
permission:
columns:
- auth0_client_id
- domain
filter: {}
limit: 1
- role: user
permission:
columns:
- auth0_client_id
- email_domain
- id
- name
- uuid
- email_domain
filter:
users:
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 Contacts from './pages/Contacts';
import Insights from './pages/Insights';
@ -7,40 +8,59 @@ import AppLayout from './layout/AppLayout';
import RequireAuth from './components/RequireAuth';
import { Routes, Route } from 'react-router-dom';
import { useGetProfile } from './hooks/profile/useGetProfile';
import { useGetTenantByDomain } from './hooks/tenant/useGetTenantByDomain';
function App() {
const { tenant } = useGetTenantByDomain();
const { user } = useGetProfile();
return (
<AppLayout user={user}>
<Routes>
<Route
path="/"
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>
</AppLayout>
<div>
{tenant && (
<Auth0Provider
domain={process.env.REACT_APP_AUTH0_DOMAIN || ''}
clientId={tenant?.auth0_client_id || ''}
authorizationParams={{
redirect_uri:
window.location.protocol +
'//' +
window.location.host +
process.env.REACT_APP_AUTH0_CALLBACK_URL || '',
audience: process.env.REACT_APP_AUTH0_AUDIENCE || '',
}}
>
<AppLayout user={user}>
<Routes>
<Route
path="/"
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>
</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 App from './App';
import { BrowserRouter } from 'react-router-dom';
import { Auth0Provider } from '@auth0/auth0-react';
import {
ApolloClient,
InMemoryCache,
@ -15,12 +14,17 @@ import { setContext } from '@apollo/client/link/context';
const httpLink = createHttpLink({ uri: process.env.REACT_APP_API_URL });
const authLink = setContext((_, { headers }) => {
const requestHeaders = { ...headers };
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 {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
headers: requestHeaders,
};
});
@ -33,22 +37,9 @@ const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
);
root.render(
<Auth0Provider
domain={process.env.REACT_APP_AUTH0_DOMAIN || ''}
clientId={process.env.REACT_APP_AUTH0_CLIENT_ID || ''}
authorizationParams={{
redirect_uri:
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>,
<ApolloProvider client={client}>
<BrowserRouter>
<App />
</BrowserRouter>
</ApolloProvider>,
);

View File

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

View File

@ -15,7 +15,12 @@ export const NavbarOnInsights = () => (
email: 'charles@twenty.com',
first_name: 'Charles',
last_name: 'Bochet',
tenant: { id: 1, name: 'Twenty' },
tenant: {
id: 1,
name: 'Twenty',
domain: 'pilot.twenty.com',
auth0_client_id: 'auth0_client_id',
},
}}
/>
</MemoryRouter>

View File

@ -36,6 +36,7 @@ services:
HASURA_GRAPHQL_PG_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/twenty
HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
HASURA_GRAPHQL_DEV_MODE: "true"
HASURA_GRAPHQL_UNAUTHORIZED_ROLE: "public"
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