Separate auth0 users depending on tenants
This commit is contained in:
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
alter table "public"."tenants" drop column "auth0_client_id";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
alter table "public"."tenants" add column "auth0_client_id" text null;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
alter table "public"."tenants" drop column "domain";
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."tenants" add column "domain" text
|
||||||
|
null default 'pilot.twenty.com';
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
31
front/src/hooks/tenant/useGetTenantByDomain.tsx
Normal file
31
front/src/hooks/tenant/useGetTenantByDomain.tsx
Normal 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] };
|
||||||
|
};
|
||||||
@ -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>,
|
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
export interface Tenant {
|
export interface Tenant {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
domain: string;
|
||||||
|
auth0_client_id: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user