@datalayer/core
Version:
[](https://datalayer.io)
169 lines (168 loc) • 9.61 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/*
* Copyright (c) 2023-2025 Datalayer, Inc.
* Distributed under the terms of the Modified BSD License.
*/
import { useEffect, useState } from 'react';
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
import { EyeIcon, EyeClosedIcon, MarkGithubIcon } from '@primer/octicons-react';
import { Button, FormControl, Heading, Link, PageLayout, TextInput, } from '@primer/react';
import { Box } from '@datalayer/primer-addons';
import { asUser, IAMProvidersSpecs } from '../../models';
import { useIAMStore } from '../../state';
import { CenteredSpinner } from '../display';
import { isInsideJupyterLab, validateLength } from '../../utils';
import { useNavigate, useCache, useToast, useIAM } from '../../hooks';
import { LoginToken } from './LoginToken';
export const Login = (props) => {
const { heading, homeRoute, loginRoute, showEmailLogin = true, showGitHubLogin = true, showTokenLogin = true, } = props;
const { useLogin, useOAuth2AuthorizationURL } = useCache({ loginRoute });
const loginMutation = useLogin();
const getOAuth2URLMutation = useOAuth2AuthorizationURL();
const { loginAndNavigate, setLogin } = useIAM();
const { externalToken, iamProvidersAuthorizationURL, addIAMProviderAuthorizationURL, iamRunUrl, logout, checkIAMToken, } = useIAMStore();
const { enqueueToast } = useToast();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [loadingWithToken, setLoadingWithToken] = useState(-1);
const [formValues, setFormValues] = useState({
handle: undefined,
password: undefined,
});
const [validationResult, setValidationResult] = useState({
handle: undefined,
password: undefined,
});
const [passwordVisibility, setPasswordVisibility] = useState(false);
useEffect(() => {
const initIAMProvider = (iamProvider) => {
const callbackURI = isInsideJupyterLab()
? URLExt.join(PageConfig.getBaseUrl(), iamProvider.oauth2CallbackServerRoute)
: location.protocol +
'//' +
location.hostname +
':' +
location.port +
iamProvider.oauth2CallbackUIRoute;
const queryArgs = {
provider: iamProvider.name,
callback_uri: callbackURI,
};
/*
const xsrfTokenMatch = document.cookie.match('\\b_xsrf=([^;]*)\\b');
if (xsrfTokenMatch) {
queryArgs['xsrf'] = xsrfTokenMatch[1];
}
*/
getOAuth2URLMutation.mutate(queryArgs, {
onSuccess: authUrl => {
if (authUrl) {
addIAMProviderAuthorizationURL(iamProvider.name, authUrl);
}
else {
console.error(`Failed to get the Login URL from Datalayer IAM for provider ${iamProvider.name}.`);
}
},
});
};
if (!iamProvidersAuthorizationURL[IAMProvidersSpecs.GitHub.name]) {
initIAMProvider(IAMProvidersSpecs.GitHub);
}
if (!iamProvidersAuthorizationURL[IAMProvidersSpecs.LinkedIn.name]) {
initIAMProvider(IAMProvidersSpecs.LinkedIn);
}
}, [iamRunUrl, iamProvidersAuthorizationURL, addIAMProviderAuthorizationURL]);
useEffect(() => {
if (externalToken) {
setLoadingWithToken(1);
loginAndNavigate(externalToken, logout, checkIAMToken, navigate, homeRoute)
.catch(error => {
console.debug('Failed to login with token from cookie..', error);
enqueueToast('Failed to check authentication.', { variant: 'error' });
})
.finally(() => {
setLoadingWithToken(-1);
});
}
}, [externalToken]);
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
submit();
}
};
const handleHandleChange = (event) => {
setFormValues(prevFormValues => ({
...prevFormValues,
handle: event.target.value,
}));
};
const handlePasswordChange = (event) => {
setFormValues(prevFormValues => ({
...prevFormValues,
password: event.target.value,
}));
};
const submit = async () => {
if (loading ||
validationResult.handle !== '' ||
validationResult.password !== '') {
return;
}
setLoading(true);
loginMutation.mutate({
handle: formValues.handle,
password: formValues.password,
}, {
onSuccess: (resp) => {
if (resp.success) {
setValidationResult({ handle: '', password: '' });
const user = asUser(resp.user);
const token = resp.token;
setLogin(user, token);
navigate(homeRoute);
}
else {
enqueueToast('Failed to login. Check your username and password.', {
variant: 'warning',
});
setValidationResult({
handle: '',
password: 'Invalid credentials',
});
console.debug(`Failed to login: ${resp.message}`, resp.errors ?? '');
}
},
onSettled: () => {
setLoading(false);
},
});
};
useEffect(() => {
setValidationResult({
...validationResult,
handle: formValues.handle === undefined
? undefined
: validateLength(formValues.handle, 1)
? ''
: 'Your username may not be empty.',
password: formValues.password === undefined
? undefined
: validateLength(formValues.password, 1)
? ''
: 'Your password may not be empty.',
});
}, [formValues]);
return (_jsxs(PageLayout, { containerWidth: "medium", padding: "normal", style: { overflow: 'visible', minHeight: 'calc(100vh - 45px)' }, children: [_jsx(PageLayout.Header, { children: _jsx(Heading, { children: heading || 'Login to Datalayer' }) }), _jsx(PageLayout.Content, { children: loadingWithToken < 0 ? (_jsx(_Fragment, { children: _jsxs(Box, { display: "flex", children: [showEmailLogin && (_jsxs(Box, { sx: { label: { marginTop: 2 }, paddingRight: '10%' }, children: [_jsx(Box, { mt: 5, children: _jsxs(FormControl, { required: true, children: [_jsx(FormControl.Label, { children: "Your username" }), _jsx(TextInput, { autoFocus: true, placeholder: "Your username", value: formValues.handle, onChange: handleHandleChange, onKeyDown: handleKeyDown }), validationResult.handle && (_jsx(FormControl.Validation, { variant: validationResult.handle ? 'error' : 'success', children: validationResult.handle }))] }) }), _jsx(Box, { children: _jsxs(FormControl, { required: true, children: [_jsx(FormControl.Label, { children: "Your password" }), _jsx(TextInput, { placeholder: "Your password", type: passwordVisibility ? 'text' : 'password', value: formValues.password, onChange: handlePasswordChange, onKeyDown: handleKeyDown, trailingAction: _jsx(TextInput.Action, { onClick: () => {
setPasswordVisibility(!passwordVisibility);
}, icon: passwordVisibility ? EyeClosedIcon : EyeIcon, "aria-label": passwordVisibility
? 'Hide password'
: 'Show password', sx: { color: 'var(--fgColor-muted)' } }), sx: { overflow: 'visible' } }), validationResult.password && (_jsx(FormControl.Validation, { variant: validationResult.password ? 'error' : 'success', children: validationResult.password }))] }) }), _jsxs(Box, { mt: 5, children: [_jsx(Button, { variant: "primary", disabled: loading ||
validationResult.handle !== '' ||
validationResult.password !== '', onClick: submit, children: loading
? 'Login…'
: heading
? 'Login with Datalayer'
: 'Login' }), _jsx(Box, { pt: 6 }), _jsx(Link, { href: "https://datalayer.app/password", target: "_blank", children: "Forgot password?" })] })] })), _jsx(Box, { children: _jsxs(Box, { display: "flex", flexDirection: "column", sx: { margin: 'auto' }, children: [showGitHubLogin &&
iamProvidersAuthorizationURL[IAMProvidersSpecs.GitHub.name] && (_jsx(Button, { leadingVisual: MarkGithubIcon, href: iamProvidersAuthorizationURL[IAMProvidersSpecs.GitHub.name], as: "a", style: { margin: '10px 0' }, children: "Login with GitHub" })), showTokenLogin && (_jsx(LoginToken, { homeRoute: homeRoute, style: { margin: '10px 0' } }))] }) })] }) })) : loadingWithToken ? (_jsx(CenteredSpinner, { message: "Checking authentication\u2026" })) : (_jsx(_Fragment, {})) })] }));
};
export default Login;