Skip to content

Commit

Permalink
Merge pull request #67 from marmelab/feat/allow-hash-router
Browse files Browse the repository at this point in the history
Feat: Handle HashRouter & BrowserRouter for Supabase redirections
  • Loading branch information
djhi authored Aug 8, 2024
2 parents 608e44a + 4affe7b commit 531eec3
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 36 deletions.
140 changes: 140 additions & 0 deletions packages/demo/public/auth-callback.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="./manifest.json" />
<link rel="shortcut icon" href="./favicon.ico" />
<title>Atomic CRM</title>
<style>
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}

.loader-container {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #fafafa;
}

/* CSS Spinner from https://projects.lukehaas.me/css-loaders/ */

.loader,
.loader:before,
.loader:after {
border-radius: 50%;
}

.loader {
color: #283593;
font-size: 11px;
text-indent: -99999em;
margin: 55px auto;
position: relative;
width: 10em;
height: 10em;
box-shadow: inset 0 0 0 1em;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}

.loader:before,
.loader:after {
position: absolute;
content: '';
}

.loader:before {
width: 5.2em;
height: 10.2em;
background: #fafafa;
border-radius: 10.2em 0 0 10.2em;
top: -0.1em;
left: -0.1em;
-webkit-transform-origin: 5.2em 5.1em;
transform-origin: 5.2em 5.1em;
-webkit-animation: load2 2s infinite ease 1.5s;
animation: load2 2s infinite ease 1.5s;
}

.loader:after {
width: 5.2em;
height: 10.2em;
background: #fafafa;
border-radius: 0 10.2em 10.2em 0;
top: -0.1em;
left: 5.1em;
-webkit-transform-origin: 0px 5.1em;
transform-origin: 0px 5.1em;
-webkit-animation: load2 2s infinite ease;
animation: load2 2s infinite ease;
}

@-webkit-keyframes load2 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

@keyframes load2 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
</style>
</head>

<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root">
<div class="loader-container">
<div class="loader">Loading...</div>
</div>
</div>
</body>
<script>
const getUrlParams = () => {
const urlSearchParams = new URLSearchParams(
window.location.hash.substring(1)
);
const access_token = urlSearchParams.get('access_token');
const refresh_token = urlSearchParams.get('refresh_token');
const type = urlSearchParams.get('type');

return { access_token, refresh_token, type };
};

function interceptAuthCallback() {
const hash = window.location.hash;
const {access_token, refresh_token, type} = getUrlParams();
window.location.href = `../#/auth-callback?access_token=${access_token}&refresh_token=${refresh_token}&type=${type}`;
}

// Call the function to intercept the auth callback
interceptAuthCallback();
</script>
</html>
51 changes: 37 additions & 14 deletions packages/ra-supabase-core/src/authProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Provider, SupabaseClient, User } from '@supabase/supabase-js';
import { AuthProvider, UserIdentity } from 'ra-core';
import { getSearchString } from './getSearchString';

export const supabaseAuthProvider = (
client: SupabaseClient,
Expand Down Expand Up @@ -93,9 +94,12 @@ export const supabaseAuthProvider = (
if (type === 'recovery' || type === 'invite') {
if (access_token && refresh_token) {
return {
redirectTo: `${
redirectTo ? `${redirectTo}/` : '/'
}set-password?access_token=${access_token}&refresh_token=${refresh_token}&type=${type}`,
redirectTo: () => ({
pathname: redirectTo
? `${redirectTo}/set-password`
: '/set-password',
search: `access_token=${access_token}&refresh_token=${refresh_token}&type=${type}`,
}),
};
}

Expand All @@ -108,24 +112,32 @@ export const supabaseAuthProvider = (
},
async checkAuth() {
// Users are on the set-password page, nothing to do
if (window.location.pathname === '/set-password') {
if (
window.location.pathname === '/set-password' ||
window.location.hash.includes('#/set-password')
) {
return;
}
// Users are on the forgot-password page, nothing to do
if (window.location.pathname === '/forgot-password') {
if (
window.location.pathname === '/forgot-password' ||
window.location.hash.includes('#/forgot-password')
) {
return;
}

const { access_token, refresh_token, type } = getUrlParams();

// Users have reset their password or have just been invited and must set a new password
if (type === 'recovery' || type === 'invite') {
if (access_token && refresh_token) {
// eslint-disable-next-line no-throw-literal
throw {
redirectTo: `${
redirectTo ? `${redirectTo}/` : '/'
}set-password?access_token=${access_token}&refresh_token=${refresh_token}&type=${type}`,
redirectTo: () => ({
pathname: redirectTo
? `${redirectTo}/set-password`
: '/set-password',
search: `access_token=${access_token}&refresh_token=${refresh_token}&type=${type}`,
}),
message: false,
};
}
Expand All @@ -145,6 +157,20 @@ export const supabaseAuthProvider = (
return Promise.resolve();
},
async getPermissions() {
if (
window.location.pathname === '/set-password' ||
window.location.hash.includes('#/set-password')
) {
return;
}
// Users are on the forgot-password page, nothing to do
if (
window.location.pathname === '/forgot-password' ||
window.location.hash.includes('#/forgot-password')
) {
return;
}

const { data, error } = await client.auth.getUser();
if (error) {
throw error;
Expand All @@ -164,7 +190,6 @@ export const supabaseAuthProvider = (
if (typeof getIdentity === 'function') {
authProvider.getIdentity = async () => {
const { data } = await client.auth.getUser();

if (data.user == null) {
throw new Error();
}
Expand Down Expand Up @@ -222,10 +247,8 @@ export type ResetPasswordParams = {
};

const getUrlParams = () => {
const urlSearchParams = new URLSearchParams(
window.location.hash.substring(1)
);

const searchStr = getSearchString();
const urlSearchParams = new URLSearchParams(searchStr);
const access_token = urlSearchParams.get('access_token');
const refresh_token = urlSearchParams.get('refresh_token');
const type = urlSearchParams.get('type');
Expand Down
10 changes: 10 additions & 0 deletions packages/ra-supabase-core/src/getSearchString.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function getSearchString() {
const search = window.location.search;
const hash = window.location.hash.substring(1);

return search && search !== ''
? search
: hash.includes('?')
? hash.split('?')[1]
: hash;
}
28 changes: 24 additions & 4 deletions packages/ra-supabase-core/src/useSupabaseAccessToken.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from './useSupabaseAccessToken';

// TODO: fix those tests
describe.skip('useSupabaseAccessToken', () => {
describe('useSupabaseAccessToken', () => {
const UseSupabaseAccessToken = (props?: UseSupabaseAccessTokenOptions) => {
const token = useSupabaseAccessToken(props);

Expand All @@ -34,6 +34,26 @@ describe.skip('useSupabaseAccessToken', () => {
});
});

test('should return the access token if present in the hash route', async () => {
window.history.pushState(
{},
'React Admin',
'/set-password#access_token=bazinga'
);

const { queryByText } = render(
<TestMemoryRouter initialEntries={['/set-password']}>
<CoreAdminContext>
<UseSupabaseAccessToken />
</CoreAdminContext>
</TestMemoryRouter>
);

await waitFor(() => {
expect(queryByText('bazinga')).not.toBeNull();
});
});

test('should return the access token from the provided key if present in the URL', async () => {
window.history.pushState(
{},
Expand All @@ -54,7 +74,7 @@ describe.skip('useSupabaseAccessToken', () => {
});
});

test('should redirect users if the access token is not present in the URL', async () => {
test.skip('should redirect users if the access token is not present in the URL', async () => {
window.history.pushState({}, 'React Admin', '/set-password');

render(
Expand All @@ -70,7 +90,7 @@ describe.skip('useSupabaseAccessToken', () => {
});
});

test('should redirect users to the provided path if the access token is not present in the URL', async () => {
test.skip('should redirect users to the provided path if the access token is not present in the URL', async () => {
window.history.pushState({}, 'React Admin', '/set-password');

render(
Expand All @@ -86,7 +106,7 @@ describe.skip('useSupabaseAccessToken', () => {
});
});

test('should not redirect users if the access token is not present in the URL and redirectTo is false', async () => {
test.skip('should not redirect users if the access token is not present in the URL and redirectTo is false', async () => {
window.history.pushState({}, 'React Admin', '/set-password');

render(
Expand Down
7 changes: 3 additions & 4 deletions packages/ra-supabase-core/src/useSupabaseAccessToken.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useRedirect } from 'ra-core';
import { useEffect } from 'react';
import { getSearchString } from './getSearchString';

/**
* This hook gets the access_token from supabase in the current browser URL and redirects to the specified page (/ by default) if there is none.
Expand Down Expand Up @@ -34,10 +35,8 @@ export const useSupabaseAccessToken = ({
}: UseSupabaseAccessTokenOptions = {}) => {
const redirect = useRedirect();

const urlSearchParams = new URLSearchParams(
window.location.search.substr(1)
);

const searchStr = getSearchString();
const urlSearchParams = new URLSearchParams(searchStr);
const access_token = urlSearchParams.get(parameterName);
useEffect(() => {
if (access_token == null) {
Expand Down
Loading

0 comments on commit 531eec3

Please sign in to comment.