export function encryptValue(value: string): Promise<{ encryptedValue: string }> {
return getJSON('/api/settings/encrypt', { value }).catch(throwGlobalError);
}
+
+export function getLoginMessage(): Promise<{ message: string }> {
+ return getJSON('/api/settings/login_message').catch(throwGlobalError);
+}
info50: '#ECF6FE',
info100: '#D9EDF7',
+ info200: '#B1DFF3',
info400: '#4B9FD5',
info500: '#0271B9',
font-size: 24px;
font-weight: 300;
}
+
+.login-message {
+ width: 450px;
+ background-color: var(--info50);
+ border: 1px solid var(--info200);
+ border-radius: 2px;
+ color: rgba(0, 0, 0, 0.87);
+}
import * as React from 'react';
import { Location } from '../../../components/hoc/withRouter';
import { Alert } from '../../../components/ui/Alert';
+import DeferredSpinner from '../../../components/ui/DeferredSpinner';
import { translate } from '../../../helpers/l10n';
+import { sanitizeString } from '../../../helpers/sanitize';
import { getReturnUrl } from '../../../helpers/urls';
import { IdentityProvider } from '../../../types/types';
import './Login.css';
export interface LoginProps {
identityProviders: IdentityProvider[];
+ loading: boolean;
+ message?: string;
onSubmit: (login: string, password: string) => Promise<void>;
location: Location;
}
export default function Login(props: LoginProps) {
- const { identityProviders, location } = props;
+ const { identityProviders, loading, location, message } = props;
const returnTo = getReturnUrl(location);
const displayError = Boolean(location.query.authorizationError);
return (
<div className="login-page" id="login_form">
- <h1 className="login-title text-center huge-spacer-bottom">
+ <h1 className="login-title text-center big-spacer-bottom">
{translate('login.login_to_sonarqube')}
</h1>
- {displayError && (
- <Alert className="huge-spacer-bottom" display="block" variant="error">
- {translate('login.unauthorized_access_alert')}
- </Alert>
- )}
+ {loading ? (
+ <DeferredSpinner loading={loading} timeout={0} />
+ ) : (
+ <>
+ {displayError && (
+ <Alert className="big-spacer-bottom" display="block" variant="error">
+ {translate('login.unauthorized_access_alert')}
+ </Alert>
+ )}
- {identityProviders.length > 0 && (
- <OAuthProviders identityProviders={identityProviders} returnTo={returnTo} />
- )}
+ {message && (
+ <div
+ className="login-message markdown big-padded spacer-top huge-spacer-bottom"
+ // eslint-disable-next-line react/no-danger
+ dangerouslySetInnerHTML={{ __html: sanitizeString(message) }}
+ />
+ )}
- <LoginForm collapsed={identityProviders.length > 0} onSubmit={props.onSubmit} />
+ {identityProviders.length > 0 && (
+ <OAuthProviders identityProviders={identityProviders} returnTo={returnTo} />
+ )}
+
+ <LoginForm collapsed={identityProviders.length > 0} onSubmit={props.onSubmit} />
+ </>
+ )}
</div>
);
}
*/
import * as React from 'react';
import { logIn } from '../../../api/auth';
+import { getLoginMessage } from '../../../api/settings';
import { getIdentityProviders } from '../../../api/users';
+import withAvailableFeatures, {
+ WithAvailableFeaturesProps
+} from '../../../app/components/available-features/withAvailableFeatures';
import { Location, withRouter } from '../../../components/hoc/withRouter';
import { addGlobalErrorMessage } from '../../../helpers/globalMessages';
import { translate } from '../../../helpers/l10n';
import { getReturnUrl } from '../../../helpers/urls';
+import { Feature } from '../../../types/features';
import { IdentityProvider } from '../../../types/types';
import Login from './Login';
-interface Props {
+interface Props extends WithAvailableFeaturesProps {
location: Location;
}
interface State {
- identityProviders?: IdentityProvider[];
+ identityProviders: IdentityProvider[];
+ loading: boolean;
+ message?: string;
}
export class LoginContainer extends React.PureComponent<Props, State> {
mounted = false;
- state: State = {};
+ state: State = {
+ identityProviders: [],
+ loading: true
+ };
componentDidMount() {
this.mounted = true;
- getIdentityProviders().then(
+ this.loadData();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ async loadData() {
+ await Promise.all([this.loadIdentityProviders(), this.loadLoginMessage()]);
+ this.setState({ loading: false });
+ }
+
+ loadIdentityProviders() {
+ return getIdentityProviders().then(
({ identityProviders }) => {
if (this.mounted) {
this.setState({ identityProviders });
);
}
- componentWillUnmount() {
- this.mounted = false;
+ async loadLoginMessage() {
+ if (this.props.hasFeature(Feature.LoginMessage)) {
+ try {
+ const { message } = await getLoginMessage();
+ if (this.mounted) {
+ this.setState({ message });
+ }
+ } catch (_) {
+ /* already handled */
+ }
+ }
}
handleSuccessfulLogin = () => {
render() {
const { location } = this.props;
- const { identityProviders } = this.state;
-
- if (!identityProviders) {
- return null;
- }
+ const { identityProviders, loading, message } = this.state;
return (
<Login
identityProviders={identityProviders}
+ loading={loading}
+ message={message}
onSubmit={this.handleSubmit}
location={location}
/>
}
}
-export default withRouter(LoginContainer);
+export default withAvailableFeatures(withRouter(LoginContainer));
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as React from 'react';
+import { getLoginMessage } from '../../../../api/settings';
import { getIdentityProviders } from '../../../../api/users';
import { addGlobalErrorMessage } from '../../../../helpers/globalMessages';
import { mockLocation } from '../../../../helpers/testMocks';
logIn: jest.fn((_id, password) => (password === 'valid' ? Promise.resolve() : Promise.reject()))
}));
+jest.mock('../../../../api/settings', () => ({
+ getLoginMessage: jest
+ .fn()
+ .mockResolvedValue({ message: 'Welcome to SQ! Please use your Skynet credentials' })
+}));
+
jest.mock('../../../../helpers/globalMessages', () => ({
addGlobalErrorMessage: jest.fn()
}));
beforeEach(jest.clearAllMocks);
it('should behave correctly', async () => {
- renderLoginContainer();
const user = userEvent.setup();
+ renderLoginContainer();
+
const heading = await screen.findByRole('heading', { name: 'login.login_to_sonarqube' });
expect(heading).toBeInTheDocument();
expect(screen.getByText('login.unauthorized_access_alert')).toBeInTheDocument();
});
-function renderLoginContainer(props: Partial<LoginContainer['props']> = {}) {
+it('should display a login message if enabled & provided', async () => {
+ renderLoginContainer({}, true);
+
+ const message = await screen.findByText('Welcome to SQ! Please use your Skynet credentials');
+ expect(message).toBeInTheDocument();
+});
+
+it('should handle errors', async () => {
+ (getLoginMessage as jest.Mock).mockRejectedValueOnce('nope');
+ renderLoginContainer({}, true);
+
+ const heading = await screen.findByRole('heading', { name: 'login.login_to_sonarqube' });
+ expect(heading).toBeInTheDocument();
+});
+
+function renderLoginContainer(
+ props: Partial<LoginContainer['props']> = {},
+ loginMessageEnabled = false
+) {
return renderComponent(
- <LoginContainer location={mockLocation({ query: { return_to: '/some/path' } })} {...props} />
+ <LoginContainer
+ hasFeature={jest.fn(() => loginMessageEnabled)}
+ location={mockLocation({ query: { return_to: '/some/path' } })}
+ {...props}
+ />
);
}
user.login_or_email_used_as_scm_account=Login and email are automatically considered as SCM accounts
user.x_deleted={0} (deleted)
-login.login_to_sonarqube=Log In to SonarQube
+login.login_to_sonarqube=Log in to SonarQube
login.login_with_x=Log in with {0}
login.more_options=More options
login.unauthorized_access_alert=You are not authorized to access this page. Please log in with more privileges and try again.