diff --git a/pkg/api/api.go b/pkg/api/api.go
index c73331ce84d..bd0a771f731 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -88,6 +88,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/alerting/*", reqEditorRole, hs.Index)
// sign up
+ r.Get("/verify", hs.Index)
r.Get("/signup", hs.Index)
r.Get("/api/user/signup/options", Wrap(GetSignUpOptions))
r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp))
diff --git a/public/app/core/components/ForgottenPassword/ForgottenPassword.tsx b/public/app/core/components/ForgottenPassword/ForgottenPassword.tsx
index 51dce749063..88e067c6354 100644
--- a/public/app/core/components/ForgottenPassword/ForgottenPassword.tsx
+++ b/public/app/core/components/ForgottenPassword/ForgottenPassword.tsx
@@ -3,7 +3,6 @@ import { Form, Field, Input, Button, Legend, Container, useStyles, HorizontalGro
import { getBackendSrv } from '@grafana/runtime';
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
-
import config from 'app/core/config';
interface EmailDTO {
@@ -21,6 +20,7 @@ const paragraphStyles = (theme: GrafanaTheme) => css`
export const ForgottenPassword: FC = () => {
const [emailSent, setEmailSent] = useState(false);
const styles = useStyles(paragraphStyles);
+ const loginHref = `${config.appSubUrl}/login`;
const sendEmail = async (formModel: EmailDTO) => {
const res = await getBackendSrv().post('/api/user/password/send-reset-email', formModel);
@@ -34,7 +34,7 @@ export const ForgottenPassword: FC = () => {
An email with a reset link has been sent to the email address. You should receive it shortly.
-
+
Back to login
@@ -55,7 +55,7 @@ export const ForgottenPassword: FC = () => {
-
+
Back to login
diff --git a/public/app/core/components/Login/LoginPage.tsx b/public/app/core/components/Login/LoginPage.tsx
index 7ae220a4d46..f5d90bb7645 100644
--- a/public/app/core/components/Login/LoginPage.tsx
+++ b/public/app/core/components/Login/LoginPage.tsx
@@ -11,7 +11,6 @@ import { ChangePassword } from '../ForgottenPassword/ChangePassword';
import { Branding } from 'app/core/components/Branding/Branding';
import { HorizontalGroup, LinkButton } from '@grafana/ui';
import { LoginLayout, InnerBox } from './LoginLayout';
-
import config from 'app/core/config';
const forgottenPasswordStyles = css`
diff --git a/public/app/core/components/Login/UserSignup.tsx b/public/app/core/components/Login/UserSignup.tsx
index 8819f5ccdfb..7487ad1283d 100644
--- a/public/app/core/components/Login/UserSignup.tsx
+++ b/public/app/core/components/Login/UserSignup.tsx
@@ -1,8 +1,10 @@
import React, { FC } from 'react';
import { LinkButton, VerticalGroup } from '@grafana/ui';
import { css } from 'emotion';
+import { getConfig } from 'app/core/config';
export const UserSignup: FC<{}> = () => {
+ const href = getConfig().verifyEmailEnabled ? `${getConfig().appSubUrl}/verify` : `${getConfig().appSubUrl}/signup`;
return (
= () => {
width: 100%;
justify-content: center;
`}
- href="signup"
+ href={href}
variant="secondary"
>
Sign Up
diff --git a/public/app/core/components/Signup/Signup.tsx b/public/app/core/components/Signup/Signup.tsx
new file mode 100644
index 00000000000..91fc707e3bf
--- /dev/null
+++ b/public/app/core/components/Signup/Signup.tsx
@@ -0,0 +1,126 @@
+import React, { FC } from 'react';
+import { connect, MapStateToProps } from 'react-redux';
+import { StoreState } from 'app/types';
+import { Form, Field, Input, Button, HorizontalGroup, LinkButton } from '@grafana/ui';
+import { getConfig } from 'app/core/config';
+import { getBackendSrv } from '@grafana/runtime';
+import appEvents from 'app/core/app_events';
+import { AppEvents } from '@grafana/data';
+
+interface SignupDTO {
+ name: string;
+ email: string;
+ username: string;
+ orgName?: string;
+ password: string;
+ code: string;
+ confirm: string;
+}
+
+interface ConnectedProps {
+ email?: string;
+ code?: string;
+}
+
+const SignupUnconnected: FC = props => {
+ const onSubmit = async (formData: SignupDTO) => {
+ if (formData.name === '') {
+ delete formData.name;
+ }
+ delete formData.confirm;
+
+ const response = await getBackendSrv()
+ .post('/api/user/signup/step2', {
+ email: formData.email,
+ code: formData.code,
+ username: formData.email,
+ orgName: formData.orgName,
+ password: formData.password,
+ name: formData.name,
+ })
+ .catch(err => {
+ const msg = err.data?.message || err;
+ appEvents.emit(AppEvents.alertWarning, [msg]);
+ });
+
+ if (response.code === 'redirect-to-select-org') {
+ window.location.href = getConfig().appSubUrl + '/profile/select-org?signup=1';
+ }
+ window.location.href = getConfig().appSubUrl + '/';
+ };
+
+ const defaultValues = {
+ email: props.email,
+ code: props.code,
+ };
+
+ return (
+
+ );
+};
+
+const mapStateToProps: MapStateToProps = (state: StoreState) => ({
+ email: state.location.routeParams.email?.toString(),
+ code: state.location.routeParams.code?.toString(),
+});
+
+export const Signup = connect(mapStateToProps)(SignupUnconnected);
diff --git a/public/app/core/components/Signup/SignupPage.tsx b/public/app/core/components/Signup/SignupPage.tsx
new file mode 100644
index 00000000000..1855b1a0d5d
--- /dev/null
+++ b/public/app/core/components/Signup/SignupPage.tsx
@@ -0,0 +1,15 @@
+import React, { FC } from 'react';
+import { LoginLayout, InnerBox } from '../Login/LoginLayout';
+import { Signup } from './Signup';
+
+export const SignupPage: FC = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default SignupPage;
diff --git a/public/app/core/components/Signup/VerifyEmail.tsx b/public/app/core/components/Signup/VerifyEmail.tsx
new file mode 100644
index 00000000000..d82b0213c4b
--- /dev/null
+++ b/public/app/core/components/Signup/VerifyEmail.tsx
@@ -0,0 +1,62 @@
+import React, { FC, useState } from 'react';
+import { Form, Field, Input, Button, Legend, Container, HorizontalGroup, LinkButton } from '@grafana/ui';
+import { getConfig } from 'app/core/config';
+import { getBackendSrv } from '@grafana/runtime';
+import appEvents from 'app/core/app_events';
+import { AppEvents } from '@grafana/data';
+
+interface EmailDTO {
+ email: string;
+}
+
+export const VerifyEmail: FC = () => {
+ const [emailSent, setEmailSent] = useState(false);
+
+ const onSubmit = (formModel: EmailDTO) => {
+ getBackendSrv()
+ .post('/api/user/signup', formModel)
+ .then(() => {
+ setEmailSent(true);
+ })
+ .catch(err => {
+ const msg = err.data?.message || err;
+ appEvents.emit(AppEvents.alertWarning, [msg]);
+ });
+ };
+
+ if (emailSent) {
+ return (
+
+
An email with a verification link has been sent to the email address. You should receive it shortly.
+
+
+ Complete Signup
+
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/public/app/core/components/Signup/VerifyEmailPage.tsx b/public/app/core/components/Signup/VerifyEmailPage.tsx
new file mode 100644
index 00000000000..e1c024ea16d
--- /dev/null
+++ b/public/app/core/components/Signup/VerifyEmailPage.tsx
@@ -0,0 +1,22 @@
+import React, { FC } from 'react';
+
+import { LoginLayout, InnerBox } from '../Login/LoginLayout';
+import { VerifyEmail } from './VerifyEmail';
+import { getConfig } from 'app/core/config';
+
+export const VerifyEmailPage: FC = () => {
+ if (!getConfig().verifyEmailEnabled) {
+ window.location.href = getConfig().appSubUrl + '/signup';
+ return <>>;
+ }
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default VerifyEmailPage;
diff --git a/public/app/features/profile/SignupForm.test.tsx b/public/app/features/profile/SignupForm.test.tsx
deleted file mode 100644
index 4c3822b7147..00000000000
--- a/public/app/features/profile/SignupForm.test.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import { SignupForm } from './SignupForm';
-
-describe('SignupForm', () => {
- describe('With different values for verifyEmail and autoAssignOrg', () => {
- it('should render input fields', () => {
- const wrapper = shallow();
- expect(wrapper.exists('Forms.Input[name="orgName"]'));
- expect(wrapper.exists('Forms.Input[name="code"]'));
- });
- it('should not render input fields', () => {
- const wrapper = shallow();
- expect(wrapper.exists('Forms.Input[name="orgName"]')).toBeFalsy();
- expect(wrapper.exists('Forms.Input[name="code"]')).toBeFalsy();
- });
- });
-});
diff --git a/public/app/features/profile/SignupForm.tsx b/public/app/features/profile/SignupForm.tsx
deleted file mode 100644
index 5f83d9622ff..00000000000
--- a/public/app/features/profile/SignupForm.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import React, { FC } from 'react';
-import { Button, LinkButton, Input, Form, Field } from '@grafana/ui';
-import { css } from 'emotion';
-
-import { getConfig } from 'app/core/config';
-import { getBackendSrv } from '@grafana/runtime';
-
-interface SignupFormModel {
- email: string;
- username?: string;
- password: string;
- orgName: string;
- code?: string;
- name?: string;
-}
-interface Props {
- email?: string;
- orgName?: string;
- username?: string;
- code?: string;
- name?: string;
- verifyEmailEnabled?: boolean;
- autoAssignOrg?: boolean;
-}
-
-const buttonSpacing = css`
- margin-left: 15px;
-`;
-
-export const SignupForm: FC = props => {
- const verifyEmailEnabled = props.verifyEmailEnabled;
- const autoAssignOrg = props.autoAssignOrg;
-
- const onSubmit = async (formData: SignupFormModel) => {
- if (formData.name === '') {
- delete formData.name;
- }
-
- const response = await getBackendSrv().post('/api/user/signup/step2', {
- email: formData.email,
- code: formData.code,
- username: formData.email,
- orgName: formData.orgName,
- password: formData.password,
- name: formData.name,
- });
-
- if (response.code === 'redirect-to-select-org') {
- window.location.href = getConfig().appSubUrl + '/profile/select-org?signup=1';
- }
- window.location.href = getConfig().appSubUrl + '/';
- };
-
- const defaultValues = {
- orgName: props.orgName,
- email: props.email,
- username: props.email,
- code: props.code,
- name: props.name,
- };
-
- return (
-
- );
-};
diff --git a/public/app/features/profile/SignupPage.tsx b/public/app/features/profile/SignupPage.tsx
deleted file mode 100644
index 50554188f28..00000000000
--- a/public/app/features/profile/SignupPage.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React, { FC } from 'react';
-import { SignupForm } from './SignupForm';
-import Page from 'app/core/components/Page/Page';
-import { getConfig } from 'app/core/config';
-import { connect } from 'react-redux';
-import { hot } from 'react-hot-loader';
-import { StoreState } from 'app/types';
-
-const navModel = {
- main: {
- icon: 'grafana',
- text: 'Sign Up',
- subTitle: 'Register your Grafana account',
- breadcrumbs: [{ title: 'Login', url: 'login' }],
- },
- node: {
- text: '',
- },
-};
-
-interface Props {
- email?: string;
- orgName?: string;
- username?: string;
- code?: string;
- name?: string;
-}
-export const SignupPage: FC = props => {
- return (
-
-
- You're almost there.
-
- We just need a couple of more bits of
-
information to finish creating your account.
-
-
-
-
- );
-};
-
-const mapStateToProps = (state: StoreState) => ({
- ...state.location.routeParams,
-});
-
-export default hot(module)(connect(mapStateToProps)(SignupPage));
diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts
index c874fe3d12a..185cd3882b6 100644
--- a/public/app/routes/routes.ts
+++ b/public/app/routes/routes.ts
@@ -4,7 +4,6 @@ import { applyRouteRegistrationHandlers } from './registry';
// Pages
import LdapPage from 'app/features/admin/ldap/LdapPage';
import UserAdminPage from 'app/features/admin/UserAdminPage';
-import SignupPage from 'app/features/profile/SignupPage';
import { LoginPage } from 'app/core/components/Login/LoginPage';
import config from 'app/core/config';
@@ -436,13 +435,25 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
SafeDynamicImport(import(/* webpackChunkName: "SignupInvited" */ 'app/features/users/SignupInvited')),
},
})
- .when('/signup', {
- template: '',
- //@ts-ignore
- pageClass: 'sidemenu-hidden',
+ .when('/verify', {
+ template: '',
resolve: {
- component: () => SignupPage,
+ component: () =>
+ SafeDynamicImport(
+ import(/* webpackChunkName: "VerifyEmailPage" */ 'app/core/components/Signup/VerifyEmailPage')
+ ),
},
+ // @ts-ignore
+ pageClass: 'login-page sidemenu-hidden',
+ })
+ .when('/signup', {
+ template: '',
+ resolve: {
+ component: () =>
+ SafeDynamicImport(import(/* webpackChunkName: "SignupPage" */ 'app/core/components/Signup/SignupPage')),
+ },
+ // @ts-ignore
+ pageClass: 'login-page sidemenu-hidden',
})
.when('/user/password/send-reset-email', {
template: '',