summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile6
-rw-r--r--api/user.go34
-rw-r--r--api/user_test.go20
-rw-r--r--config/config.json2
-rw-r--r--doc/developer/tests/test-attachments.md7
-rw-r--r--docker/dev/config_docker.json2
-rw-r--r--docker/local/config_docker.json2
-rw-r--r--model/client.go8
-rw-r--r--model/config.go17
-rw-r--r--utils/config.go2
-rw-r--r--web/react/components/admin_console/email_settings.jsx84
-rw-r--r--web/react/components/center_panel.jsx2
-rw-r--r--web/react/components/get_link_modal.jsx23
-rw-r--r--web/react/components/get_team_invite_link_modal.jsx12
-rw-r--r--web/react/components/login.jsx15
-rw-r--r--web/react/components/login_username.jsx181
-rw-r--r--web/react/components/signup_user_complete.jsx10
-rw-r--r--web/react/components/team_general_tab.jsx4
-rw-r--r--web/react/components/textbox.jsx10
-rw-r--r--web/react/stores/user_store.jsx10
-rw-r--r--web/react/utils/client.jsx22
-rw-r--r--web/static/i18n/en.json13
22 files changed, 458 insertions, 28 deletions
diff --git a/Makefile b/Makefile
index 9c4e6ee1f..eccdf39ba 100644
--- a/Makefile
+++ b/Makefile
@@ -151,7 +151,7 @@ package:
sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
sed -i'.bak' 's|libs.min.js|libs-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
rm $(DIST_PATH)/web/templates/*.bak
-
+
sudo mv -f $(DIST_PATH)/config/config.json.bak $(DIST_PATH)/config/config.json || echo 'nomv'
tar -C dist -czf $(DIST_PATH).tar.gz mattermost
@@ -283,7 +283,7 @@ run: start-docker .prepare-go .prepare-jsx
$(GO) run $(GOFLAGS) mattermost.go -config=config.json &
@echo Starting compass watch
- cd web/sass-files && compass watch &
+ cd web/sass-files && compass compile && compass watch &
stop:
@for PID in $$(ps -ef | grep [c]ompass | awk '{ print $$2 }'); do \
@@ -296,7 +296,7 @@ stop:
kill $$PID; \
done
- @for PID in $$(ps -ef | grep [m]atterm | awk '{ print $$2 }'); do \
+ @for PID in $$(ps -ef | grep [m]atterm | grep -v VirtualBox | awk '{ print $$2 }'); do \
echo stopping go web $$PID; \
kill $$PID; \
done
diff --git a/api/user.go b/api/user.go
index 6ad0f67ac..91c8c022a 100644
--- a/api/user.go
+++ b/api/user.go
@@ -444,6 +444,38 @@ func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, nam
return nil
}
+func LoginByUsername(c *Context, w http.ResponseWriter, r *http.Request, username, name, password, deviceId string) *model.User {
+ var team *model.Team
+
+ if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
+ c.Err = result.Err
+ return nil
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ if result := <-Srv.Store.User().GetByUsername(team.Id, username); result.Err != nil {
+ c.Err = result.Err
+ c.Err.StatusCode = http.StatusForbidden
+ return nil
+ } else {
+ user := result.Data.(*model.User)
+
+ if len(user.AuthData) != 0 {
+ c.Err = model.NewLocAppError("LoginByUsername", "api.user.login_by_email.sign_in.app_error",
+ map[string]interface{}{"AuthService": user.AuthService}, "")
+ return nil
+ }
+
+ if checkUserLoginAttempts(c, user) && checkUserPassword(c, user, password) {
+ Login(c, w, r, user, deviceId)
+ return user
+ }
+ }
+
+ return nil
+}
+
func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.ReadCloser, team *model.Team) *model.User {
authData := ""
provider := einterfaces.GetOauthProvider(service)
@@ -629,6 +661,8 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
user = LoginById(c, w, r, props["id"], props["password"], props["device_id"])
} else if len(props["email"]) != 0 && len(props["name"]) != 0 {
user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["device_id"])
+ } else if len(props["username"]) != 0 && len(props["name"]) != 0 {
+ user = LoginByUsername(c, w, r, props["username"], props["name"], props["password"], props["device_id"])
} else {
c.Err = model.NewLocAppError("login", "api.user.login.not_provided.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
diff --git a/api/user_test.go b/api/user_test.go
index b2ae113f1..1a1cf9634 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -99,7 +99,7 @@ func TestLogin(t *testing.T) {
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
- user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "pwd"}
+ user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Username: "corey", Password: "pwd"}
ruser, _ := Client.CreateUser(&user, "")
store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
@@ -107,7 +107,7 @@ func TestLogin(t *testing.T) {
t.Fatal(err)
} else {
if result.Data.(*model.User).Email != user.Email {
- t.Fatal("email's didn't match")
+ t.Fatal("emails didn't match")
}
}
@@ -119,14 +119,30 @@ func TestLogin(t *testing.T) {
}
}
+ if result, err := Client.LoginByUsername(team.Name, user.Username, user.Password); err != nil {
+ t.Fatal(err)
+ } else {
+ if result.Data.(*model.User).Email != user.Email {
+ t.Fatal("emails didn't match")
+ }
+ }
+
if _, err := Client.LoginByEmail(team.Name, user.Email, user.Password+"invalid"); err == nil {
t.Fatal("Invalid Password")
}
+ if _, err := Client.LoginByUsername(team.Name, user.Username, user.Password+"invalid"); err == nil {
+ t.Fatal("Invalid Password")
+ }
+
if _, err := Client.LoginByEmail(team.Name, "", user.Password); err == nil {
t.Fatal("should have failed")
}
+ if _, err := Client.LoginByUsername(team.Name, "", user.Password); err == nil {
+ t.Fatal("should have failed")
+ }
+
authToken := Client.AuthToken
Client.AuthToken = "invalid"
diff --git a/config/config.json b/config/config.json
index 076f795cc..560073ad2 100644
--- a/config/config.json
+++ b/config/config.json
@@ -66,6 +66,8 @@
},
"EmailSettings": {
"EnableSignUpWithEmail": true,
+ "EnableSignInWithEmail": true,
+ "EnableSignInWithUsername": false,
"SendEmailNotifications": false,
"RequireEmailVerification": false,
"FeedbackName": "",
diff --git a/doc/developer/tests/test-attachments.md b/doc/developer/tests/test-attachments.md
index e2fda0eb6..75a2285f8 100644
--- a/doc/developer/tests/test-attachments.md
+++ b/doc/developer/tests/test-attachments.md
@@ -7,8 +7,9 @@ This test contains instructions for the core team to manually test common attach
**Notes:**
- All file types should upload and post.
-- Read the expected for details on the behavior of the thumbnail and preview window.
+- Read the expected for details on the behavior of the thumbnail and preview window.
- The expected behavior of video and audio formats depends on the operating system, browser and plugins. View the permalinks to the Public Test Channel on Pre-Release Core to see the expected cases.
+- If the browser can play the media file, media player controls should appear. If the browser cannot play the file, it should show appear as a regular attachment without the media controls.
### Images
@@ -72,7 +73,7 @@ Expected: Generic Word thumbnail & preview window.
**MP4**
`Videos/MP4.mp4`
-Expected: Generic video thumbnail & playable preview window. View Permalink.
+Expected: Generic video thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/5dx5qx9t9brqfnhohccxjynx7c)
**AVI**
@@ -114,7 +115,7 @@ Expected: Generic audio thumbnail & playable preview window
**M4A**
`Audio/M4a.m4a`
-Expected: Generic audio thumbnail & playable preview window
+Expected: Generic audio thumbnail, view Permalink for preview window behavior. Expected depends on the operating system, browser and plugins.
[Permalink](https://pre-release.mattermost.com/core/pl/6c7qsw48ybd88bktgeykodsrrc)
**AAC**
diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json
index 1aa2ee843..80b99b66d 100644
--- a/docker/dev/config_docker.json
+++ b/docker/dev/config_docker.json
@@ -66,6 +66,8 @@
},
"EmailSettings": {
"EnableSignUpWithEmail": true,
+ "EnableSignInWithEmail": true,
+ "EnableSignInWithUsername": false,
"SendEmailNotifications": false,
"RequireEmailVerification": false,
"FeedbackName": "",
diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json
index 1aa2ee843..80b99b66d 100644
--- a/docker/local/config_docker.json
+++ b/docker/local/config_docker.json
@@ -66,6 +66,8 @@
},
"EmailSettings": {
"EnableSignUpWithEmail": true,
+ "EnableSignInWithEmail": true,
+ "EnableSignInWithUsername": false,
"SendEmailNotifications": false,
"RequireEmailVerification": false,
"FeedbackName": "",
diff --git a/model/client.go b/model/client.go
index d31ac1592..3b72f65e4 100644
--- a/model/client.go
+++ b/model/client.go
@@ -280,6 +280,14 @@ func (c *Client) LoginByEmail(name string, email string, password string) (*Resu
return c.login(m)
}
+func (c *Client) LoginByUsername(name string, username string, password string) (*Result, *AppError) {
+ m := make(map[string]string)
+ m["name"] = name
+ m["username"] = username
+ m["password"] = password
+ return c.login(m)
+}
+
func (c *Client) LoginByEmailWithDevice(name string, email string, password string, deviceId string) (*Result, *AppError) {
m := make(map[string]string)
m["name"] = name
diff --git a/model/config.go b/model/config.go
index 5c8604ff1..a6d1c21dc 100644
--- a/model/config.go
+++ b/model/config.go
@@ -97,6 +97,8 @@ type FileSettings struct {
type EmailSettings struct {
EnableSignUpWithEmail bool
+ EnableSignInWithEmail *bool
+ EnableSignInWithUsername *bool
SendEmailNotifications bool
RequireEmailVerification bool
FeedbackName string
@@ -258,6 +260,21 @@ func (o *Config) SetDefaults() {
*o.TeamSettings.EnableTeamListing = false
}
+ if o.EmailSettings.EnableSignInWithEmail == nil {
+ o.EmailSettings.EnableSignInWithEmail = new(bool)
+
+ if o.EmailSettings.EnableSignUpWithEmail == true {
+ *o.EmailSettings.EnableSignInWithEmail = true
+ } else {
+ *o.EmailSettings.EnableSignInWithEmail = false
+ }
+ }
+
+ if o.EmailSettings.EnableSignInWithUsername == nil {
+ o.EmailSettings.EnableSignInWithUsername = new(bool)
+ *o.EmailSettings.EnableSignInWithUsername = false
+ }
+
if o.EmailSettings.SendPushNotifications == nil {
o.EmailSettings.SendPushNotifications = new(bool)
*o.EmailSettings.SendPushNotifications = false
diff --git a/utils/config.go b/utils/config.go
index 9d2c2f588..e9b7e1878 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -208,6 +208,8 @@ func getClientConfig(c *model.Config) map[string]string {
props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications)
props["EnableSignUpWithEmail"] = strconv.FormatBool(c.EmailSettings.EnableSignUpWithEmail)
+ props["EnableSignInWithEmail"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithEmail)
+ props["EnableSignInWithUsername"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithUsername)
props["RequireEmailVerification"] = strconv.FormatBool(c.EmailSettings.RequireEmailVerification)
props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail
diff --git a/web/react/components/admin_console/email_settings.jsx b/web/react/components/admin_console/email_settings.jsx
index ce3c8cd12..17f25a04c 100644
--- a/web/react/components/admin_console/email_settings.jsx
+++ b/web/react/components/admin_console/email_settings.jsx
@@ -112,6 +112,8 @@ class EmailSettings extends React.Component {
buildConfig() {
var config = this.props.config;
config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked;
+ config.EmailSettings.EnableSignInWithEmail = ReactDOM.findDOMNode(this.refs.allowSignInWithEmail).checked;
+ config.EmailSettings.EnableSignInWithUsername = ReactDOM.findDOMNode(this.refs.allowSignInWithUsername).checked;
config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
config.EmailSettings.SendPushNotifications = ReactDOM.findDOMNode(this.refs.sendPushNotifications).checked;
config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked;
@@ -320,6 +322,88 @@ class EmailSettings extends React.Component {
<div className='form-group'>
<label
className='control-label col-sm-4'
+ htmlFor='allowSignInWithEmail'
+ >
+ <FormattedMessage
+ id='admin.email.allowEmailSignInTitle'
+ defaultMessage='Allow Sign In With Email: '
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='allowSignInWithEmail'
+ value='true'
+ ref='allowSignInWithEmail'
+ defaultChecked={this.props.config.EmailSettings.EnableSignInWithEmail}
+ onChange={this.handleChange.bind(this, 'allowSignInWithEmail_true')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='allowSignInWithEmail'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.EnableSignInWithEmail}
+ onChange={this.handleChange.bind(this, 'allowSignInWithEmail_false')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.email.allowEmailSignInDescription'
+ defaultMessage='When true, Mattermost allows users to sign in using their email and password.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
+ htmlFor='allowSignInWithUsername'
+ >
+ <FormattedMessage
+ id='admin.email.allowUsernameSignInTitle'
+ defaultMessage='Allow Sign In With Username: '
+ />
+ </label>
+ <div className='col-sm-8'>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='allowSignInWithUsername'
+ value='true'
+ ref='allowSignInWithUsername'
+ defaultChecked={this.props.config.EmailSettings.EnableSignInWithUsername}
+ onChange={this.handleChange.bind(this, 'allowSignInWithUsername_true')}
+ />
+ {'true'}
+ </label>
+ <label className='radio-inline'>
+ <input
+ type='radio'
+ name='allowSignInWithUsername'
+ value='false'
+ defaultChecked={!this.props.config.EmailSettings.EnableSignInWithUsername}
+ onChange={this.handleChange.bind(this, 'allowSignInWithUsername_false')}
+ />
+ {'false'}
+ </label>
+ <p className='help-text'>
+ <FormattedMessage
+ id='admin.email.allowUsernameSignInDescription'
+ defaultMessage='When true, Mattermost allows users to sign in using their username and password. This setting is typically only used when email verification is disabled.'
+ />
+ </p>
+ </div>
+ </div>
+
+ <div className='form-group'>
+ <label
+ className='control-label col-sm-4'
htmlFor='sendEmailNotifications'
>
<FormattedMessage
diff --git a/web/react/components/center_panel.jsx b/web/react/components/center_panel.jsx
index 53dad1306..443ecefde 100644
--- a/web/react/components/center_panel.jsx
+++ b/web/react/components/center_panel.jsx
@@ -69,7 +69,7 @@ export default class CenterPanel extends React.Component {
onClick={handleClick}
>
<a href=''>
- {'You are viewing the Archives. Click here to jump to recent messages. '}
+ {'Click here to jump to recent messages. '}
{<i className='fa fa-arrow-down'></i>}
</a>
</div>
diff --git a/web/react/components/get_link_modal.jsx b/web/react/components/get_link_modal.jsx
index 3fc71ff96..de3387a35 100644
--- a/web/react/components/get_link_modal.jsx
+++ b/web/react/components/get_link_modal.jsx
@@ -41,6 +41,8 @@ export default class GetLinkModal extends React.Component {
}
render() {
+ const userCreationEnabled = global.window.mm_config.EnableUserCreation === 'true';
+
let helpText = null;
if (this.props.helpText) {
helpText = (
@@ -53,7 +55,7 @@ export default class GetLinkModal extends React.Component {
}
let copyLink = null;
- if (document.queryCommandSupported('copy')) {
+ if (userCreationEnabled && document.queryCommandSupported('copy')) {
copyLink = (
<button
data-copy-btn='true'
@@ -69,6 +71,18 @@ export default class GetLinkModal extends React.Component {
);
}
+ let linkText = null;
+ if (userCreationEnabled) {
+ linkText = (
+ <textarea
+ className='form-control no-resize min-height'
+ readOnly='true'
+ ref='textarea'
+ value={this.props.link}
+ />
+ );
+ }
+
var copyLinkConfirm = null;
if (this.state.copiedLink) {
copyLinkConfirm = (
@@ -92,12 +106,7 @@ export default class GetLinkModal extends React.Component {
</Modal.Header>
<Modal.Body>
{helpText}
- <textarea
- className='form-control no-resize min-height'
- readOnly='true'
- ref='textarea'
- value={this.props.link}
- />
+ {linkText}
</Modal.Body>
<Modal.Footer>
<button
diff --git a/web/react/components/get_team_invite_link_modal.jsx b/web/react/components/get_team_invite_link_modal.jsx
index 883871267..299729250 100644
--- a/web/react/components/get_team_invite_link_modal.jsx
+++ b/web/react/components/get_team_invite_link_modal.jsx
@@ -16,6 +16,10 @@ const holders = defineMessages({
help: {
id: 'get_team_invite_link_modal.help',
defaultMessage: 'Send teammates the link below for them to sign-up to this team site.'
+ },
+ helpDisabled: {
+ id: 'get_team_invite_link_modal.helpDisabled',
+ defaultMessage: 'User creation has been disabled for your team. Please ask your team administrator for details.'
}
});
@@ -47,12 +51,18 @@ class GetTeamInviteLinkModal extends React.Component {
render() {
const {formatMessage} = this.props.intl;
+ let helpText = formatMessage(holders.helpDisabled);
+
+ if (global.window.mm_config.EnableUserCreation === 'true') {
+ helpText = formatMessage(holders.help);
+ }
+
return (
<GetLinkModal
show={this.state.show}
onHide={() => this.setState({show: false})}
title={formatMessage(holders.title)}
- helpText={formatMessage(holders.help)}
+ helpText={helpText}
link={TeamStore.getCurrentInviteLink()}
/>
);
diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx
index c4f530af0..0123a0f3c 100644
--- a/web/react/components/login.jsx
+++ b/web/react/components/login.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
import LoginEmail from './login_email.jsx';
+import LoginUsername from './login_username.jsx';
import LoginLdap from './login_ldap.jsx';
import * as Utils from '../utils/utils.jsx';
@@ -35,7 +36,7 @@ export default class Login extends React.Component {
/>
</span>
</a>
- );
+ );
}
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
@@ -87,7 +88,7 @@ export default class Login extends React.Component {
}
let emailSignup;
- if (global.window.mm_config.EnableSignUpWithEmail === 'true') {
+ if (global.window.mm_config.EnableSignInWithEmail === 'true') {
emailSignup = (
<LoginEmail
teamName={this.props.teamName}
@@ -189,6 +190,15 @@ export default class Login extends React.Component {
);
}
+ let usernameLogin = null;
+ if (global.window.mm_config.EnableSignInWithUsername === 'true') {
+ usernameLogin = (
+ <LoginUsername
+ teamName={this.props.teamName}
+ />
+ );
+ }
+
return (
<div className='signup-team__container'>
<h5 className='margin--less'>
@@ -210,6 +220,7 @@ export default class Login extends React.Component {
{extraBox}
{loginMessage}
{emailSignup}
+ {usernameLogin}
{ldapLogin}
{userSignUp}
{findTeams}
diff --git a/web/react/components/login_username.jsx b/web/react/components/login_username.jsx
new file mode 100644
index 000000000..f787490fa
--- /dev/null
+++ b/web/react/components/login_username.jsx
@@ -0,0 +1,181 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as Utils from '../utils/utils.jsx';
+import * as Client from '../utils/client.jsx';
+import UserStore from '../stores/user_store.jsx';
+
+import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'mm-intl';
+
+var holders = defineMessages({
+ badTeam: {
+ id: 'login_username.badTeam',
+ defaultMessage: 'Bad team name'
+ },
+ usernameReq: {
+ id: 'login_username.usernameReq',
+ defaultMessage: 'A username is required'
+ },
+ pwdReq: {
+ id: 'login_username.pwdReq',
+ defaultMessage: 'A password is required'
+ },
+ verifyEmailError: {
+ id: 'login_username.verifyEmailError',
+ defaultMessage: 'Please verify your email address. Check your inbox for an email.'
+ },
+ userNotFoundError: {
+ id: 'login_username.userNotFoundError',
+ defaultMessage: "We couldn't find an existing account matching your username for this team."
+ },
+ username: {
+ id: 'login_username.username',
+ defaultMessage: 'Username'
+ },
+ pwd: {
+ id: 'login_username.pwd',
+ defaultMessage: 'Password'
+ }
+});
+
+export default class LoginUsername extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleSubmit = this.handleSubmit.bind(this);
+
+ this.state = {
+ serverError: ''
+ };
+ }
+ handleSubmit(e) {
+ e.preventDefault();
+ const {formatMessage} = this.props.intl;
+ var state = {};
+
+ const name = this.props.teamName;
+ if (!name) {
+ state.serverError = formatMessage(holders.badTeam);
+ this.setState(state);
+ return;
+ }
+
+ const username = this.refs.username.value.trim();
+ if (!username) {
+ state.serverError = formatMessage(holders.usernameReq);
+ this.setState(state);
+ return;
+ }
+
+ const password = this.refs.password.value.trim();
+ if (!password) {
+ state.serverError = formatMessage(holders.pwdReq);
+ this.setState(state);
+ return;
+ }
+
+ state.serverError = '';
+ this.setState(state);
+
+ Client.loginByUsername(name, username, password,
+ () => {
+ UserStore.setLastUsername(username);
+
+ const redirect = Utils.getUrlParameter('redirect');
+ if (redirect) {
+ window.location.href = decodeURIComponent(redirect);
+ } else {
+ window.location.href = '/' + name + '/channels/town-square';
+ }
+ },
+ (err) => {
+ if (err.message === 'api.user.login.not_verified.app_error') {
+ state.serverError = formatMessage(holders.verifyEmailError);
+ } else if (err.message === 'store.sql_user.get_by_username.app_error') {
+ state.serverError = formatMessage(holders.userNotFoundError);
+ } else {
+ state.serverError = err.message;
+ }
+
+ this.valid = false;
+ this.setState(state);
+ }
+ );
+ }
+ render() {
+ let serverError;
+ let errorClass = '';
+ if (this.state.serverError) {
+ serverError = <label className='control-label'>{this.state.serverError}</label>;
+ errorClass = ' has-error';
+ }
+
+ let priorUsername = UserStore.getLastUsername();
+ let focusUsername = false;
+ let focusPassword = false;
+ if (priorUsername === '') {
+ focusUsername = true;
+ } else {
+ focusPassword = true;
+ }
+
+ const emailParam = Utils.getUrlParameter('email');
+ if (emailParam) {
+ priorUsername = decodeURIComponent(emailParam);
+ }
+
+ const {formatMessage} = this.props.intl;
+ return (
+ <form onSubmit={this.handleSubmit}>
+ <div className='signup__email-container'>
+ <div className={'form-group' + errorClass}>
+ {serverError}
+ </div>
+ <div className={'form-group' + errorClass}>
+ <input
+ autoFocus={focusUsername}
+ type='username'
+ className='form-control'
+ name='username'
+ defaultValue={priorUsername}
+ ref='username'
+ placeholder={formatMessage(holders.username)}
+ spellCheck='false'
+ />
+ </div>
+ <div className={'form-group' + errorClass}>
+ <input
+ autoFocus={focusPassword}
+ type='password'
+ className='form-control'
+ name='password'
+ ref='password'
+ placeholder={formatMessage(holders.pwd)}
+ spellCheck='false'
+ />
+ </div>
+ <div className='form-group'>
+ <button
+ type='submit'
+ className='btn btn-primary'
+ >
+ <FormattedMessage
+ id='login_username.signin'
+ defaultMessage='Sign in'
+ />
+ </button>
+ </div>
+ </div>
+ </form>
+ );
+ }
+}
+LoginUsername.defaultProps = {
+};
+
+LoginUsername.propTypes = {
+ intl: intlShape.isRequired,
+ teamName: React.PropTypes.string.isRequired
+};
+
+export default injectIntl(LoginUsername);
diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx
index 47ec58e98..98a832542 100644
--- a/web/react/components/signup_user_complete.jsx
+++ b/web/react/components/signup_user_complete.jsx
@@ -150,9 +150,18 @@ class SignupUserComplete extends React.Component {
// set up error labels
var emailError = null;
+ var emailHelpText = (
+ <span className='help-block'>
+ <FormattedMessage
+ id='signup_user_completed.emailHelp'
+ defaultMessage='Valid email required for sign-up'
+ />
+ </span>
+ );
var emailDivStyle = 'form-group';
if (this.state.emailError) {
emailError = <label className='control-label'>{this.state.emailError}</label>;
+ emailHelpText = '';
emailDivStyle += ' has-error';
}
@@ -232,6 +241,7 @@ class SignupUserComplete extends React.Component {
spellCheck='false'
/>
{emailError}
+ {emailHelpText}
</div>
</div>
);
diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx
index 0656d3b03..0a1b02853 100644
--- a/web/react/components/team_general_tab.jsx
+++ b/web/react/components/team_general_tab.jsx
@@ -575,6 +575,8 @@ class GeneralTab extends React.Component {
</div>
);
+ const nameExtraInfo = <span>{formatMessage(holders.teamNameInfo)}</span>;
+
nameSection = (
<SettingItemMax
title={formatMessage({id: 'general_tab.teamName'})}
@@ -583,7 +585,7 @@ class GeneralTab extends React.Component {
server_error={serverError}
client_error={clientError}
updateSection={this.onUpdateNameSection}
- extraInfo={formatMessage(holders.teamNameInfo)}
+ extraInfo={nameExtraInfo}
/>
);
} else {
diff --git a/web/react/components/textbox.jsx b/web/react/components/textbox.jsx
index bb383aca1..00e5ace98 100644
--- a/web/react/components/textbox.jsx
+++ b/web/react/components/textbox.jsx
@@ -129,13 +129,6 @@ export default class Textbox extends React.Component {
this.resize();
}
- showHelp(e) {
- e.preventDefault();
- e.target.blur();
-
- global.window.open('/docs/Messaging');
- }
-
render() {
let previewLink = null;
if (Utils.isFeatureEnabled(PreReleaseFeatures.MARKDOWN_PREVIEW)) {
@@ -194,7 +187,8 @@ export default class Textbox extends React.Component {
</div>
{previewLink}
<a
- onClick={this.showHelp}
+ target='_blank'
+ href='http://docs.mattermost.com/help/getting-started/messaging-basics.html'
className='textbox-help-link'
>
<FormattedMessage
diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx
index 3e1871180..b97a0d87b 100644
--- a/web/react/stores/user_store.jsx
+++ b/web/react/stores/user_store.jsx
@@ -38,6 +38,8 @@ class UserStoreClass extends EventEmitter {
this.setCurrentUser = this.setCurrentUser.bind(this);
this.getLastEmail = this.getLastEmail.bind(this);
this.setLastEmail = this.setLastEmail.bind(this);
+ this.getLastUsername = this.getLastUsername.bind(this);
+ this.setLastUsername = this.setLastUsername.bind(this);
this.hasProfile = this.hasProfile.bind(this);
this.getProfile = this.getProfile.bind(this);
this.getProfileByUsername = this.getProfileByUsername.bind(this);
@@ -159,6 +161,14 @@ class UserStoreClass extends EventEmitter {
BrowserStore.setGlobalItem('last_email', email);
}
+ getLastUsername() {
+ return BrowserStore.getGlobalItem('last_username', '');
+ }
+
+ setLastUsername(username) {
+ BrowserStore.setGlobalItem('last_username', username);
+ }
+
hasProfile(userId) {
return this.getProfiles()[userId] != null;
}
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index 09cd4162a..c4b1bc061 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -305,6 +305,28 @@ export function loginByEmail(name, email, password, success, error) {
});
}
+export function loginByUsername(name, username, password, success, error) {
+ $.ajax({
+ url: '/api/v1/users/login',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'POST',
+ data: JSON.stringify({name, username, password}),
+ success: function onSuccess(data, textStatus, xhr) {
+ track('api', 'api_users_login_success', data.team_id, 'username', data.username);
+ sessionStorage.removeItem(data.id + '_last_error');
+ BrowserStore.signalLogin();
+ success(data, textStatus, xhr);
+ },
+ error: function onError(xhr, status, err) {
+ track('api', 'api_users_login_fail', name, 'username', username);
+
+ var e = handleError('loginByUsername', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function loginByLdap(teamName, id, password, success, error) {
$.ajax({
url: '/api/v1/users/login_ldap',
diff --git a/web/static/i18n/en.json b/web/static/i18n/en.json
index d6401ab6e..de955349e 100644
--- a/web/static/i18n/en.json
+++ b/web/static/i18n/en.json
@@ -127,6 +127,10 @@
"admin.email.true": "true",
"admin.email.false": "false",
"admin.email.allowSignupDescription": "When true, Mattermost allows team creation and account signup using email and password. This value should be false only when you want to limit signup to a single-sign-on service like OAuth or LDAP.",
+ "admin.email.allowEmailSignInTitle": "Allow Sign In With Email: ",
+ "admin.email.allowEmailSignInDescription": "When true, Mattermost allows users to sign in using their email and password.",
+ "admin.email.allowUsernameSignInTitle": "Allow Sign In With Username: ",
+ "admin.email.allowUsernameSignInDescription": "When true, Mattermost allows users to sign in using their username and password. This setting is typically only used when email verification is disabled.",
"admin.email.notificationsTitle": "Send Email Notifications: ",
"admin.email.notificationsDescription": "Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.<br />Setting this to true removes the Preview Mode banner (requires logging out and logging back in after setting is changed).",
"admin.email.requireVerificationTitle": "Require Email Verification: ",
@@ -526,6 +530,7 @@
"get_link.close": "Close",
"get_team_invite_link_modal.title": "Team Invite Link",
"get_team_invite_link_modal.help": "Send teammates the link below for them to sign-up to this team site.",
+ "get_team_invite_link_modal.helpDisabled": "User creation has been disabled for your team. Please ask your team administrator for details.",
"invite_member.emailError": "Please enter a valid email address",
"invite_member.firstname": "First name",
"invite_member.lastname": "Last name",
@@ -550,6 +555,14 @@
"login_email.email": "Email",
"login_email.pwd": "Password",
"login_email.signin": "Sign in",
+ "login_username.badTeam": "Bad team name",
+ "login_username.usernameReq": "A username is required",
+ "login_username.pwdReq": "A password is required",
+ "login_username.verifyEmailError": "Please verify your email address. Check your inbox for an email.",
+ "login_username.userNotFoundError": "We couldn't find an existing account matching your username for this team.",
+ "login_username.username": "Username",
+ "login_username.pwd": "Password",
+ "login_username.signin": "Sign in",
"login_ldap.badTeam": "Bad team name",
"login_ldap.idlReq": "An LDAP ID is required",
"login_ldap.pwdReq": "An LDAP password is required",