Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cecfec0b7 | |||
| ec663b601f | |||
| 4de26c331b | |||
| f66a8e3f1d | |||
| 4f3e0ef5b3 | |||
| 7b363d4624 |
@@ -467,6 +467,8 @@ class Base(Configuration):
|
||||
)
|
||||
|
||||
# OIDC - Authorization Code Flow
|
||||
OIDC_AUTHENTICATE_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationRequestView"
|
||||
OIDC_CALLBACK_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationCallbackView"
|
||||
OIDC_CREATE_USER = values.BooleanValue(
|
||||
default=True,
|
||||
environ_name="OIDC_CREATE_USER",
|
||||
@@ -551,6 +553,9 @@ class Base(Configuration):
|
||||
environ_name="OIDC_STORE_REFRESH_TOKEN_KEY",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_REDIRECT_FIELD_NAME = values.Value(
|
||||
"returnTo", environ_name="OIDC_REDIRECT_FIELD_NAME", environ_prefix=None
|
||||
)
|
||||
|
||||
# WARNING: Enabling this setting allows multiple user accounts to share the same email
|
||||
# address. This may cause security issues and is not recommended for production use when
|
||||
|
||||
@@ -4,11 +4,13 @@ import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Auth, KEY_AUTH, setAuthUrl } from '@/features/auth';
|
||||
import { Auth, KEY_AUTH } from '@/features/auth';
|
||||
import { useResponsiveStore } from '@/stores/';
|
||||
|
||||
import { ConfigProvider } from './config/';
|
||||
|
||||
export const DEFAULT_QUERY_RETRY = 1;
|
||||
|
||||
/**
|
||||
* QueryClient:
|
||||
* - defaultOptions:
|
||||
@@ -19,7 +21,7 @@ import { ConfigProvider } from './config/';
|
||||
const defaultOptions = {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 3,
|
||||
retry: 1,
|
||||
retry: DEFAULT_QUERY_RETRY,
|
||||
},
|
||||
};
|
||||
const queryClient = new QueryClient({
|
||||
@@ -51,8 +53,9 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
|
||||
void queryClient.resetQueries({
|
||||
queryKey: [KEY_AUTH],
|
||||
});
|
||||
setAuthUrl();
|
||||
void replace(`/401`);
|
||||
void replace(
|
||||
`/401?returnTo=${encodeURIComponent(window.location.pathname)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -38,7 +38,10 @@ export const getConfig = async (): Promise<ConfigResponse> => {
|
||||
const response = await fetchAPI(`config/`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError('Failed to get the doc', await errorCauses(response));
|
||||
throw new APIError(
|
||||
'Failed to get the configurations',
|
||||
await errorCauses(response),
|
||||
);
|
||||
}
|
||||
|
||||
const config = response.json() as Promise<ConfigResponse>;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { DEFAULT_QUERY_RETRY } from '@/core';
|
||||
import {
|
||||
attemptSilentLogin,
|
||||
canAttemptSilentLogin,
|
||||
} from '@/features/auth/silentLogin';
|
||||
|
||||
import { User } from './types';
|
||||
|
||||
@@ -16,6 +21,16 @@ import { User } from './types';
|
||||
*/
|
||||
export const getMe = async (): Promise<User> => {
|
||||
const response = await fetchAPI(`users/me/`);
|
||||
|
||||
if (!response.ok && response.status == 401 && canAttemptSilentLogin()) {
|
||||
const currentLocation = window.location.href;
|
||||
attemptSilentLogin(3600);
|
||||
|
||||
while (window.location.href === currentLocation) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError(
|
||||
`Couldn't fetch user data: ${response.statusText}`,
|
||||
@@ -34,6 +49,13 @@ export function useAuthQuery(
|
||||
queryKey: [KEY_AUTH],
|
||||
queryFn: getMe,
|
||||
staleTime: 1000 * 60 * 15, // 15 minutes
|
||||
retry: (failureCount, error) => {
|
||||
// we assume that a 401 means the user is not logged in
|
||||
if (error.status == 401) {
|
||||
return false;
|
||||
}
|
||||
return failureCount < DEFAULT_QUERY_RETRY;
|
||||
},
|
||||
...queryConfig,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useConfig } from '@/core';
|
||||
|
||||
import { HOME_URL } from '../conf';
|
||||
import { useAuth } from '../hooks';
|
||||
import { getAuthUrl, gotoLogin } from '../utils';
|
||||
import { gotoLogin } from '../utils';
|
||||
|
||||
export const Auth = ({ children }: PropsWithChildren) => {
|
||||
const { isLoading, pathAllowed, isFetchedAfterMount, authenticated } =
|
||||
@@ -23,22 +23,6 @@ export const Auth = ({ children }: PropsWithChildren) => {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user is authenticated and wanted initially to access a document,
|
||||
* we redirect to the document page.
|
||||
*/
|
||||
if (authenticated) {
|
||||
const authUrl = getAuthUrl();
|
||||
if (authUrl) {
|
||||
void replace(authUrl);
|
||||
return (
|
||||
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user is not authenticated and the path is not allowed, we redirect to the login page.
|
||||
*/
|
||||
|
||||
@@ -3,4 +3,3 @@ import { baseApiUrl } from '@/api';
|
||||
export const HOME_URL = '/home';
|
||||
export const LOGIN_URL = `${baseApiUrl()}authenticate/`;
|
||||
export const LOGOUT_URL = `${baseApiUrl()}logout/`;
|
||||
export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth';
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { gotoLogin } from '@/features/auth';
|
||||
|
||||
const SILENT_LOGIN_RETRY_KEY = 'silent-login-retry';
|
||||
|
||||
const isRetryAllowed = () => {
|
||||
const lastRetryDate = localStorage.getItem(SILENT_LOGIN_RETRY_KEY);
|
||||
if (!lastRetryDate) {
|
||||
return true;
|
||||
}
|
||||
const now = new Date();
|
||||
return now.getTime() > Number(lastRetryDate);
|
||||
};
|
||||
|
||||
const setNextRetryTime = (retryIntervalInSeconds: number) => {
|
||||
const now = new Date();
|
||||
const nextRetryTime = now.getTime() + retryIntervalInSeconds * 1000;
|
||||
localStorage.setItem(SILENT_LOGIN_RETRY_KEY, String(nextRetryTime));
|
||||
};
|
||||
|
||||
const initiateSilentLogin = () => {
|
||||
const currentPath = window.location.pathname;
|
||||
gotoLogin(currentPath, true);
|
||||
};
|
||||
|
||||
export const canAttemptSilentLogin = () => {
|
||||
return isRetryAllowed();
|
||||
};
|
||||
|
||||
export const attemptSilentLogin = (retryIntervalInSeconds: number) => {
|
||||
if (!isRetryAllowed()) {
|
||||
return;
|
||||
}
|
||||
setNextRetryTime(retryIntervalInSeconds);
|
||||
initiateSilentLogin();
|
||||
};
|
||||
@@ -1,27 +1,12 @@
|
||||
import { terminateCrispSession } from '@/services/Crisp';
|
||||
|
||||
import { LOGIN_URL, LOGOUT_URL, PATH_AUTH_LOCAL_STORAGE } from './conf';
|
||||
import { LOGIN_URL, LOGOUT_URL } from './conf';
|
||||
|
||||
export const getAuthUrl = () => {
|
||||
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
if (path_auth) {
|
||||
localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
|
||||
return path_auth;
|
||||
}
|
||||
};
|
||||
|
||||
export const setAuthUrl = () => {
|
||||
if (window.location.pathname !== '/') {
|
||||
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname);
|
||||
}
|
||||
};
|
||||
|
||||
export const gotoLogin = (withRedirect = true) => {
|
||||
if (withRedirect) {
|
||||
setAuthUrl();
|
||||
}
|
||||
|
||||
window.location.replace(LOGIN_URL);
|
||||
export const gotoLogin = (returnTo = '/', isSilent = false) => {
|
||||
const authenticateUrl =
|
||||
LOGIN_URL +
|
||||
`?silent=${encodeURIComponent(isSilent)}&returnTo=${window.location.origin + returnTo}`;
|
||||
window.location.replace(authenticateUrl);
|
||||
};
|
||||
|
||||
export const gotoLogout = () => {
|
||||
|
||||
@@ -19,7 +19,6 @@ export const getDoc = async ({ id }: DocParams): Promise<Doc> => {
|
||||
};
|
||||
|
||||
export const KEY_DOC = 'doc';
|
||||
export const KEY_DOC_VISIBILITY = 'doc-visibility';
|
||||
|
||||
export function useDoc(
|
||||
param: DocParams,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useConfig } from '@/core';
|
||||
import { useAuthQuery } from '@/features/auth/api';
|
||||
import { useAuthQuery } from '@/features/auth';
|
||||
import { useChangeUserLanguage } from '@/features/language/api/useChangeUserLanguage';
|
||||
import { getMatchingLocales } from '@/features/language/utils/locale';
|
||||
import { availableFrontendLanguages } from '@/i18n/initI18n';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { ReactElement, useEffect } from 'react';
|
||||
import { ReactElement, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import img401 from '@/assets/icons/icon-401.png';
|
||||
@@ -13,7 +13,18 @@ import { NextPageWithLayout } from '@/types/next';
|
||||
const Page: NextPageWithLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
const { authenticated } = useAuth();
|
||||
const { replace } = useRouter();
|
||||
const router = useRouter();
|
||||
const { replace } = router;
|
||||
|
||||
const [returnTo, setReturnTo] = useState<string | undefined>(undefined);
|
||||
const { returnTo: returnToParams } = router.query;
|
||||
|
||||
useEffect(() => {
|
||||
if (returnToParams) {
|
||||
setReturnTo(returnToParams as string);
|
||||
void replace('/401');
|
||||
}
|
||||
}, [returnToParams, replace]);
|
||||
|
||||
useEffect(() => {
|
||||
if (authenticated) {
|
||||
@@ -42,7 +53,7 @@ const Page: NextPageWithLayout = () => {
|
||||
{t('Log in to access the document.')}
|
||||
</Text>
|
||||
|
||||
<Button onClick={() => gotoLogin(false)} aria-label={t('Login')}>
|
||||
<Button onClick={() => gotoLogin(returnTo)} aria-label={t('Login')}>
|
||||
{t('Login')}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Icon, TextErrors } from '@/components';
|
||||
import { DEFAULT_QUERY_RETRY } from '@/core';
|
||||
import { DocEditor } from '@/docs/doc-editor';
|
||||
import {
|
||||
Doc,
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
useDoc,
|
||||
useDocStore,
|
||||
} from '@/docs/doc-management/';
|
||||
import { KEY_AUTH, setAuthUrl } from '@/features/auth';
|
||||
import { MainLayout } from '@/layouts';
|
||||
import { useBroadcastStore } from '@/stores';
|
||||
import { NextPageWithLayout } from '@/types/next';
|
||||
@@ -56,6 +56,14 @@ const DocPage = ({ id }: DocProps) => {
|
||||
{
|
||||
staleTime: 0,
|
||||
queryKey: [KEY_DOC, { id }],
|
||||
retryDelay: 1000,
|
||||
retry: (failureCount, error) => {
|
||||
if (error.status == 403 || error.status == 401 || error.status == 404) {
|
||||
return false;
|
||||
} else {
|
||||
return failureCount < DEFAULT_QUERY_RETRY;
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -101,23 +109,17 @@ const DocPage = ({ id }: DocProps) => {
|
||||
}, [addTask, doc?.id, queryClient]);
|
||||
|
||||
if (isError && error) {
|
||||
if (error.status === 403) {
|
||||
void replace(`/403`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error.status === 404) {
|
||||
void replace(`/404`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (error.status === 401) {
|
||||
void queryClient.resetQueries({
|
||||
queryKey: [KEY_AUTH],
|
||||
});
|
||||
setAuthUrl();
|
||||
void replace(`/401`);
|
||||
return null;
|
||||
if ([403, 404, 401].includes(error.status)) {
|
||||
void replace(
|
||||
error.status === 401
|
||||
? `/${error.status}?returnTo=${encodeURIComponent(window.location.pathname)}`
|
||||
: `/${error.status}`,
|
||||
);
|
||||
return (
|
||||
<Box $align="center" $justify="center" $height="100%">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user