diff --git a/public/app/core/components/Login/ChangePassword.tsx b/public/app/core/components/Login/ChangePassword.tsx new file mode 100644 index 0000000000000..bf0fe4e92a5ab --- /dev/null +++ b/public/app/core/components/Login/ChangePassword.tsx @@ -0,0 +1,135 @@ +import React, { PureComponent, SyntheticEvent, ChangeEvent } from 'react'; +import { Tooltip } from '@grafana/ui'; +import appEvents from 'app/core/app_events'; + +interface Props { + onSubmit: (pw: string) => void; + onSkip: Function; + focus?: boolean; +} + +interface State { + newPassword: string; + confirmNew: string; + valid: boolean; +} + +export class ChangePassword extends PureComponent { + private userInput: HTMLInputElement; + constructor(props: Props) { + super(props); + this.state = { + newPassword: '', + confirmNew: '', + valid: false, + }; + } + + componentDidUpdate(prevProps: Props) { + if (!prevProps.focus && this.props.focus) { + this.focus(); + } + } + + focus() { + this.userInput.focus(); + } + + onSubmit = (e: SyntheticEvent) => { + e.preventDefault(); + + const { newPassword, valid } = this.state; + if (valid) { + this.props.onSubmit(newPassword); + } else { + appEvents.emit('alert-warning', ['New passwords do not match', '']); + } + }; + + onNewPasswordChange = (e: ChangeEvent) => { + this.setState({ + newPassword: e.target.value, + valid: this.validate('newPassword', e.target.value), + }); + }; + + onConfirmPasswordChange = (e: ChangeEvent) => { + this.setState({ + confirmNew: e.target.value, + valid: this.validate('confirmNew', e.target.value), + }); + }; + + onSkip = (e: SyntheticEvent) => { + this.props.onSkip(); + }; + + validate(changed: string, pw: string) { + if (changed === 'newPassword') { + return this.state.confirmNew === pw; + } else if (changed === 'confirmNew') { + return this.state.newPassword === pw; + } + return false; + } + + render() { + return ( +
+
+
Change Password
+ Before you can get started with awesome dashboards we need you to make your account more secure by changing + your password. +
+ You can change your password again later. +
+
+
+ { + this.userInput = input; + }} + /> +
+
+ +
+
+ + + Skip + + + + +
+
+
+ ); + } +} diff --git a/public/app/core/components/Login/LoginCtrl.tsx b/public/app/core/components/Login/LoginCtrl.tsx new file mode 100644 index 0000000000000..b68945187da8c --- /dev/null +++ b/public/app/core/components/Login/LoginCtrl.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import config from 'app/core/config'; + +import { updateLocation } from 'app/core/actions'; +import { connect } from 'react-redux'; +import { StoreState } from 'app/types'; +import { PureComponent } from 'react'; +import { getBackendSrv } from '@grafana/runtime'; +import { hot } from 'react-hot-loader'; +import appEvents from 'app/core/app_events'; + +const isOauthEnabled = () => Object.keys(config.oauth).length > 0; + +export interface FormModel { + user: string; + password: string; + email: string; +} +interface Props { + routeParams?: any; + updateLocation?: typeof updateLocation; + children: (props: { + isLoggingIn: boolean; + changePassword: (pw: string) => void; + isChangingPassword: boolean; + skipPasswordChange: Function; + login: (data: FormModel) => void; + disableLoginForm: boolean; + ldapEnabled: boolean; + authProxyEnabled: boolean; + disableUserSignUp: boolean; + isOauthEnabled: boolean; + loginHint: string; + passwordHint: string; + }) => JSX.Element; +} + +interface State { + isLoggingIn: boolean; + isChangingPassword: boolean; +} + +export class LoginCtrl extends PureComponent { + result: any = {}; + constructor(props: Props) { + super(props); + this.state = { + isLoggingIn: false, + isChangingPassword: false, + }; + + if (config.loginError) { + appEvents.emit('alert-warning', ['Login Failed', config.loginError]); + } + } + + changePassword = (password: string) => { + const pw = { + newPassword: password, + confirmNew: password, + oldPassword: 'admin', + }; + getBackendSrv() + .put('/api/user/password', pw) + .then(() => { + this.toGrafana(); + }) + .catch((err: any) => console.log(err)); + }; + + login = (formModel: FormModel) => { + this.setState({ + isLoggingIn: true, + }); + + getBackendSrv() + .post('/login', formModel) + .then((result: any) => { + this.result = result; + if (formModel.password !== 'admin' || config.ldapEnabled || config.authProxyEnabled) { + this.toGrafana(); + return; + } else { + this.changeView(); + } + }) + .catch(() => { + this.setState({ + isLoggingIn: false, + }); + }); + }; + + changeView = () => { + this.setState({ + isChangingPassword: true, + }); + }; + + toGrafana = () => { + const params = this.props.routeParams; + // Use window.location.href to force page reload + if (params.redirect && params.redirect[0] === '/') { + window.location.href = config.appSubUrl + params.redirect; + + // this.props.updateLocation({ + // path: config.appSubUrl + params.redirect, + // }); + } else if (this.result.redirectUrl) { + window.location.href = config.appSubUrl + params.redirect; + + // this.props.updateLocation({ + // path: this.result.redirectUrl, + // }); + } else { + window.location.href = config.appSubUrl + '/'; + + // this.props.updateLocation({ + // path: '/', + // }); + } + }; + + render() { + const { children } = this.props; + const { isLoggingIn, isChangingPassword } = this.state; + const { login, toGrafana, changePassword } = this; + const { loginHint, passwordHint, disableLoginForm, ldapEnabled, authProxyEnabled, disableUserSignUp } = config; + + return ( + <> + {children({ + isOauthEnabled: isOauthEnabled(), + loginHint, + passwordHint, + disableLoginForm, + ldapEnabled, + authProxyEnabled, + disableUserSignUp, + login, + isLoggingIn, + changePassword, + skipPasswordChange: toGrafana, + isChangingPassword, + })} + + ); + } +} + +export const mapStateToProps = (state: StoreState) => ({ + routeParams: state.location.routeParams, +}); + +const mapDispatchToProps = { updateLocation }; + +export default hot(module)( + connect( + mapStateToProps, + mapDispatchToProps + )(LoginCtrl) +); diff --git a/public/app/core/components/Login/LoginForm.tsx b/public/app/core/components/Login/LoginForm.tsx new file mode 100644 index 0000000000000..03f960cedd41e --- /dev/null +++ b/public/app/core/components/Login/LoginForm.tsx @@ -0,0 +1,120 @@ +import React, { PureComponent, SyntheticEvent, ChangeEvent } from 'react'; +import { FormModel } from './LoginCtrl'; + +interface Props { + displayForgotPassword: boolean; + onChange?: (valid: boolean) => void; + onSubmit: (data: FormModel) => void; + isLoggingIn: boolean; + passwordHint: string; + loginHint: string; +} + +interface State { + user: string; + password: string; + email: string; + valid: boolean; +} + +export class LoginForm extends PureComponent { + private userInput: HTMLInputElement; + constructor(props: Props) { + super(props); + this.state = { + user: '', + password: '', + email: '', + valid: false, + }; + } + + componentDidMount() { + this.userInput.focus(); + } + onSubmit = (e: SyntheticEvent) => { + e.preventDefault(); + + const { user, password, email } = this.state; + if (this.state.valid) { + this.props.onSubmit({ user, password, email }); + } + }; + + onChangePassword = (e: ChangeEvent) => { + this.setState({ + password: e.target.value, + valid: this.validate(this.state.user, e.target.value), + }); + }; + + onChangeUsername = (e: ChangeEvent) => { + this.setState({ + user: e.target.value, + valid: this.validate(e.target.value, this.state.password), + }); + }; + + validate(user: string, password: string) { + return user.length > 0 && password.length > 0; + } + + render() { + return ( +
+
+ { + this.userInput = input; + }} + type="text" + name="user" + className="gf-form-input login-form-input" + required + placeholder={this.props.loginHint} + aria-label="Username input field" + onChange={this.onChangeUsername} + /> +
+
+ +
+
+ {!this.props.isLoggingIn ? ( + + ) : ( + + )} + + {this.props.displayForgotPassword ? ( + + ) : null} +
+
+ ); + } +} diff --git a/public/app/core/components/Login/LoginPage.tsx b/public/app/core/components/Login/LoginPage.tsx new file mode 100644 index 0000000000000..0026885adde62 --- /dev/null +++ b/public/app/core/components/Login/LoginPage.tsx @@ -0,0 +1,81 @@ +import React, { FC } from 'react'; +import { UserSignup } from './UserSignup'; +import { LoginServiceButtons } from './LoginServiceButtons'; +import LoginCtrl from './LoginCtrl'; +import { LoginForm } from './LoginForm'; +import { ChangePassword } from './ChangePassword'; +import { CSSTransition } from 'react-transition-group'; + +export const LoginPage: FC = () => { + return ( +
+
+
+ Grafana +
+
+ + {({ + loginHint, + passwordHint, + isOauthEnabled, + ldapEnabled, + authProxyEnabled, + disableLoginForm, + disableUserSignUp, + login, + isLoggingIn, + changePassword, + skipPasswordChange, + isChangingPassword, + }) => ( +
+
+ {!disableLoginForm ? ( + + ) : null} + + {isOauthEnabled ? ( + <> +
+
+
+
+
+ {disableLoginForm ? null : or} +
+
+
+
+
+
+ + + + ) : null} + {!disableUserSignUp ? : null} +
+ + + +
+ )} + + +
+
+
+ ); +}; diff --git a/public/app/core/components/Login/LoginServiceButtons.tsx b/public/app/core/components/Login/LoginServiceButtons.tsx new file mode 100644 index 0000000000000..42c05a0b7a1e8 --- /dev/null +++ b/public/app/core/components/Login/LoginServiceButtons.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import config from 'app/core/config'; + +const loginServices: () => LoginServices = () => ({ + saml: { + enabled: config.samlEnabled, + name: 'SAML', + className: 'github', + icon: 'key', + }, + google: { + enabled: config.oauth.google, + name: 'Google', + }, + github: { + enabled: config.oauth.github, + name: 'GitHub', + }, + gitlab: { + enabled: config.oauth.gitlab, + name: 'GitLab', + }, + grafanacom: { + enabled: config.oauth.grafana_com, + name: 'Grafana.com', + hrefName: 'grafana_com', + icon: 'grafana_com', + }, + oauth: { + enabled: config.oauth.generic_oauth, + name: 'OAuth', + icon: 'sign-in', + hrefName: 'generic_oauth', + }, +}); + +export interface LoginService { + enabled: boolean; + name: string; + hrefName?: string; + icon?: string; + className?: string; +} + +export interface LoginServices { + [key: string]: LoginService; +} + +export const LoginServiceButtons = () => { + const keyNames = Object.keys(loginServices()); + const serviceElements = keyNames.map(key => { + const service: LoginService = loginServices()[key]; + return service.enabled ? ( + + + Sign in with {service.name} + + ) : null; + }); + + return
{serviceElements}
; +}; diff --git a/public/app/core/components/Login/UserSignup.tsx b/public/app/core/components/Login/UserSignup.tsx new file mode 100644 index 0000000000000..1551a77410366 --- /dev/null +++ b/public/app/core/components/Login/UserSignup.tsx @@ -0,0 +1,12 @@ +import React, { FC } from 'react'; + +export const UserSignup: FC<{}> = () => { + return ( +
+
New to Grafana?
+ + Sign Up + +
+ ); +}; diff --git a/public/app/core/controllers/all.ts b/public/app/core/controllers/all.ts index f6a4e51bad44e..483b6f2b02c9c 100644 --- a/public/app/core/controllers/all.ts +++ b/public/app/core/controllers/all.ts @@ -1,5 +1,4 @@ import './json_editor_ctrl'; -import './login_ctrl'; import './invited_ctrl'; import './signup_ctrl'; import './reset_password_ctrl'; diff --git a/public/app/core/controllers/login_ctrl.ts b/public/app/core/controllers/login_ctrl.ts deleted file mode 100644 index 145a55170515e..0000000000000 --- a/public/app/core/controllers/login_ctrl.ts +++ /dev/null @@ -1,145 +0,0 @@ -import _ from 'lodash'; -import coreModule from '../core_module'; -import config from 'app/core/config'; -import { BackendSrv } from '../services/backend_srv'; - -export class LoginCtrl { - /** @ngInject */ - constructor($scope: any, backendSrv: BackendSrv, $location: any) { - $scope.formModel = { - user: '', - email: '', - password: '', - }; - - $scope.command = {}; - $scope.result = ''; - $scope.loggingIn = false; - - $scope.oauth = config.oauth; - $scope.oauthEnabled = _.keys(config.oauth).length > 0; - $scope.ldapEnabled = config.ldapEnabled; - $scope.authProxyEnabled = config.authProxyEnabled; - $scope.samlEnabled = config.samlEnabled; - - $scope.disableLoginForm = config.disableLoginForm; - $scope.disableUserSignUp = config.disableUserSignUp; - $scope.loginHint = config.loginHint; - $scope.passwordHint = config.passwordHint; - - $scope.loginMode = true; - $scope.submitBtnText = 'Log in'; - - $scope.init = () => { - $scope.$watch('loginMode', $scope.loginModeChanged); - - if (config.loginError) { - $scope.appEvent('alert-warning', ['Login Failed', config.loginError]); - } - }; - - $scope.submit = () => { - if ($scope.loginMode) { - $scope.login(); - } else { - $scope.signUp(); - } - }; - - $scope.changeView = () => { - const loginView = document.querySelector('#login-view'); - const changePasswordView = document.querySelector('#change-password-view'); - - loginView.className += ' add'; - setTimeout(() => { - loginView.className += ' hidden'; - }, 250); - setTimeout(() => { - changePasswordView.classList.remove('hidden'); - }, 251); - setTimeout(() => { - changePasswordView.classList.remove('remove'); - }, 301); - - setTimeout(() => { - document.getElementById('newPassword').focus(); - }, 400); - }; - - $scope.changePassword = () => { - $scope.command.oldPassword = 'admin'; - - if ($scope.command.newPassword !== $scope.command.confirmNew) { - $scope.appEvent('alert-warning', ['New passwords do not match', '']); - return; - } - - backendSrv.put('/api/user/password', $scope.command).then(() => { - $scope.toGrafana(); - }); - }; - - $scope.skip = () => { - $scope.toGrafana(); - }; - - $scope.loginModeChanged = (newValue: boolean) => { - $scope.submitBtnText = newValue ? 'Log in' : 'Sign up'; - }; - - $scope.signUp = () => { - if (!$scope.loginForm.$valid) { - return; - } - - backendSrv.post('/api/user/signup', $scope.formModel).then((result: any) => { - if (result.status === 'SignUpCreated') { - $location.path('/signup').search({ email: $scope.formModel.email }); - } else { - window.location.href = config.appSubUrl + '/'; - } - }); - }; - - $scope.login = () => { - delete $scope.loginError; - - if (!$scope.loginForm.$valid) { - return; - } - $scope.loggingIn = true; - - backendSrv - .post('/login', $scope.formModel) - .then((result: any) => { - $scope.result = result; - - if ($scope.formModel.password !== 'admin' || $scope.ldapEnabled || $scope.authProxyEnabled) { - $scope.toGrafana(); - return; - } else { - $scope.changeView(); - } - }) - .catch(() => { - $scope.loggingIn = false; - }); - }; - - $scope.toGrafana = () => { - const params = $location.search(); - - if (params.redirect && params.redirect[0] === '/') { - window.location.href = config.appSubUrl + params.redirect; - } else if ($scope.result.redirectUrl) { - window.location.href = $scope.result.redirectUrl; - } else { - window.location.href = config.appSubUrl + '/'; - } - }; - - $scope.init(); - } -} - -coreModule.controller('LoginCtrl', LoginCtrl); diff --git a/public/app/partials/login.html b/public/app/partials/login.html deleted file mode 100644 index 32c88d1fee4d2..0000000000000 --- a/public/app/partials/login.html +++ /dev/null @@ -1,115 +0,0 @@ -