Procházet zdrojové kódy

SONAR-17515 Display a custom message on the login page

tags/9.8.0.63668
Jeremy Davis před 1 rokem
rodič
revize
0806043b59

+ 4
- 0
server/sonar-web/src/main/js/api/settings.ts Zobrazit soubor

@@ -111,3 +111,7 @@ export function generateSecretKey(): Promise<{ secretKey: string }> {
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);
}

+ 1
- 0
server/sonar-web/src/main/js/app/theme.js Zobrazit soubor

@@ -150,6 +150,7 @@ module.exports = {

info50: '#ECF6FE',
info100: '#D9EDF7',
info200: '#B1DFF3',
info400: '#4B9FD5',
info500: '#0271B9',


+ 8
- 0
server/sonar-web/src/main/js/apps/sessions/components/Login.css Zobrazit soubor

@@ -31,3 +31,11 @@
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);
}

+ 29
- 11
server/sonar-web/src/main/js/apps/sessions/components/Login.tsx Zobrazit soubor

@@ -20,7 +20,9 @@
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';
@@ -29,32 +31,48 @@ import OAuthProviders from './OAuthProviders';

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>
);
}

+ 42
- 12
server/sonar-web/src/main/js/apps/sessions/components/LoginContainer.tsx Zobrazit soubor

@@ -19,29 +19,52 @@
*/
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 });
@@ -53,8 +76,17 @@ export class LoginContainer extends React.PureComponent<Props, State> {
);
}

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 = () => {
@@ -72,15 +104,13 @@ export class LoginContainer extends React.PureComponent<Props, State> {

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}
/>
@@ -88,4 +118,4 @@ export class LoginContainer extends React.PureComponent<Props, State> {
}
}

export default withRouter(LoginContainer);
export default withAvailableFeatures(withRouter(LoginContainer));

+ 33
- 3
server/sonar-web/src/main/js/apps/sessions/components/__tests__/Login-it.tsx Zobrazit soubor

@@ -20,6 +20,7 @@
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';
@@ -39,6 +40,12 @@ jest.mock('../../../../api/auth', () => ({
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()
}));
@@ -67,9 +74,10 @@ afterAll(() => {
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();

@@ -137,8 +145,30 @@ it("should show a warning if there's an authorization error", async () => {
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}
/>
);
}

+ 1
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Zobrazit soubor

@@ -2051,7 +2051,7 @@ user.password_doesnt_match_confirmation=Password doesn't match confirmation.
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.

Načítá se…
Zrušit
Uložit