diff options
Diffstat (limited to 'web')
30 files changed, 560 insertions, 67 deletions
diff --git a/web/react/components/channel_header.jsx b/web/react/components/channel_header.jsx index 30435dc08..7a129f200 100644 --- a/web/react/components/channel_header.jsx +++ b/web/react/components/channel_header.jsx @@ -205,7 +205,7 @@ module.exports = React.createClass({ <th> <div className="dropdown channel-header__links"> <a href="#" className="dropdown-toggle theme" type="button" id="channel_header_right_dropdown" data-toggle="dropdown" aria-expanded="true"> - <span dangerouslySetInnerHTML={{__html: " <svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>"}} /> </a> + <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON }} /> </a> <ul className="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="channel_header_right_dropdown"> <li role="presentation"><a role="menuitem" href="#" onClick={this.searchMentions}>Recent Mentions</a></li> </ul> diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx index 3f8e9ed2e..88c01c586 100644 --- a/web/react/components/create_comment.jsx +++ b/web/react/components/create_comment.jsx @@ -40,7 +40,7 @@ module.exports = React.createClass({ post.parent_id = this.props.parentId; post.filenames = this.state.previews; - this.setState({ submitting: true }); + this.setState({ submitting: true, limit_error: null }); client.createPost(post, ChannelStore.getCurrent(), function(data) { diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx index 91d070958..87895588e 100644 --- a/web/react/components/create_post.jsx +++ b/web/react/components/create_post.jsx @@ -45,7 +45,7 @@ module.exports = React.createClass({ return; } - this.setState({ submitting: true }); + this.setState({ submitting: true, limit_error: null }); var user_id = UserStore.getCurrentId(); @@ -275,7 +275,7 @@ module.exports = React.createClass({ messageText = draft['message']; uploadsInProgress = draft['uploadsInProgress']; } - this.setState({ channel_id: channel_id, messageText: messageText, initialText: messageText, submitting: false, post_error: null, previews: previews, uploadsInProgress: uploadsInProgress }); + this.setState({ channel_id: channel_id, messageText: messageText, initialText: messageText, submitting: false, limit_error: null, server_error: null, post_error: null, previews: previews, uploadsInProgress: uploadsInProgress }); } }, _onActiveThreadChanged: function(rootId, parentId) { diff --git a/web/react/components/edit_channel_modal.jsx b/web/react/components/edit_channel_modal.jsx index d055feacd..a35a531b5 100644 --- a/web/react/components/edit_channel_modal.jsx +++ b/web/react/components/edit_channel_modal.jsx @@ -30,12 +30,19 @@ module.exports = React.createClass({ handleUserInput: function(e) { this.setState({ description: e.target.value }); }, + handleClose: function() { + this.setState({description: "", server_error: ""}); + }, componentDidMount: function() { var self = this; $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { var button = e.relatedTarget; self.setState({ description: $(button).attr('data-desc'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), server_error: "" }); }); + $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', this.handleClose) + }, + componentWillUnmount: function() { + $(this.refs.modal.getDOMNode()).off('hidden.bs.modal', this.handleClose) }, getInitialState: function() { return { description: "", title: "", channel_id: "" }; diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx index f2429f17e..aee089dbc 100644 --- a/web/react/components/file_upload.jsx +++ b/web/react/components/file_upload.jsx @@ -15,7 +15,7 @@ module.exports = React.createClass({ // This looks redundant, but must be done this way due to // setState being an asynchronous call var numFiles = 0; - for(var i = 0; i < files.length && i < Constants.MAX_UPLOAD_FILES; i++) { + for(var i = 0; i < files.length; i++) { if (files[i].size <= Constants.MAX_FILE_SIZE) { numFiles++; } diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 71fefff5b..05918650b 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -90,6 +90,17 @@ module.exports = React.createClass({ focusEmail = true; } + var auth_services = JSON.parse(this.props.authServices); + + var login_message; + if (auth_services.indexOf("gitlab") >= 0) { + login_message = ( + <div className="form-group form-group--small"> + <span><a href={"/"+teamName+"/login/gitlab"}>{"Log in with GitLab"}</a></span> + </div> + ); + } + return ( <div className="signup-team__container"> <div> @@ -112,6 +123,7 @@ module.exports = React.createClass({ <div className="form-group"> <button type="submit" className="btn btn-primary">Sign in</button> </div> + { login_message } <div className="form-group form-group--small"> <span><a href="/find_team">{"Find other " + strings.TeamPlural}</a></span> </div> diff --git a/web/react/components/navbar.jsx b/web/react/components/navbar.jsx index 500fabb0e..6d23c0d9b 100644 --- a/web/react/components/navbar.jsx +++ b/web/react/components/navbar.jsx @@ -191,7 +191,7 @@ module.exports = React.createClass({ </button>; var right_sidebar_collapse_button= currentId == null ? null : <button type="button" className="navbar-toggle menu-toggle pull-right" data-toggle="collapse" data-target="#sidebar-nav" onClick={this.toggleRightSidebar}> - <span className="dropdown__icon"></span> + <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON }} /> </button>; diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx index d6422fe3a..8eaaf4e8c 100644 --- a/web/react/components/post_info.jsx +++ b/web/react/components/post_info.jsx @@ -4,6 +4,8 @@ var UserStore = require('../stores/user_store.jsx'); var utils = require('../utils/utils.jsx'); +var Constants = require('../utils/constants.jsx'); + module.exports = React.createClass({ getInitialState: function() { return { }; @@ -21,7 +23,7 @@ module.exports = React.createClass({ var comments = ""; var lastCommentClass = this.props.isLastComment ? " comment-icon__container__show" : " comment-icon__container__hide"; if (this.props.commentCount >= 1) { - comments = <a href="#" className={"comment-icon__container theme" + lastCommentClass} onClick={this.props.handleCommentClick}><span className="comment-icon" dangerouslySetInnerHTML={{__html: "<svg version='1.1' id='Layer_2' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='15px' height='15px' viewBox='1 1.5 15 15' enable-background='new 1 1.5 15 15' xml:space='preserve'> <g> <g> <path fill='#211B1B' d='M14,1.5H3c-1.104,0-2,0.896-2,2v8c0,1.104,0.896,2,2,2h1.628l1.884,3l1.866-3H14c1.104,0,2-0.896,2-2v-8 C16,2.396,15.104,1.5,14,1.5z M15,11.5c0,0.553-0.447,1-1,1H8l-1.493,2l-1.504-1.991L5,12.5H3c-0.552,0-1-0.447-1-1v-8 c0-0.552,0.448-1,1-1h11c0.553,0,1,0.448,1,1V11.5z'/> </g> </g> </svg>"}} />{this.props.commentCount}</a>; + comments = <a href="#" className={"comment-icon__container theme" + lastCommentClass} onClick={this.props.handleCommentClick}><span className="comment-icon" dangerouslySetInnerHTML={{__html: Constants.COMMENT_ICON }} />{this.props.commentCount}</a>; } return ( diff --git a/web/react/components/post_right.jsx b/web/react/components/post_right.jsx index 93f5d91b0..567be1962 100644 --- a/web/react/components/post_right.jsx +++ b/web/react/components/post_right.jsx @@ -111,7 +111,7 @@ RootPost = React.createClass({ } else { postFiles.push( <div className="post-image__column custom-file" key={fileInfo.path}> - <a href={fileInfo.path+"."+ext} download={fileInfo.name+"."+ext}> + <a href={fileInfo.path+"."+fileInfo.ext} download={fileInfo.name+"."+fileInfo.ext}> <div className={"file-icon "+utils.getIconClassName(ftype)}/> </a> </div> diff --git a/web/react/components/rename_channel_modal.jsx b/web/react/components/rename_channel_modal.jsx index 2ae331626..9e4a25f85 100644 --- a/web/react/components/rename_channel_modal.jsx +++ b/web/react/components/rename_channel_modal.jsx @@ -89,12 +89,19 @@ module.exports = React.createClass({ this.refs.channel_name.getDOMNode().value = channel_name; this.setState({ channel_name: channel_name }) }, + handleClose: function() { + this.setState({display_name: "", channel_name: "", display_name_error: "", server_error: "", name_error: ""}); + }, componentDidMount: function() { var self = this; $(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { var button = $(e.relatedTarget); self.setState({ display_name: button.attr('data-display'), title: button.attr('data-name'), channel_id: button.attr('data-channelid') }); }); + $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', this.handleClose); + }, + componentWillUnmount: function() { + $(this.refs.modal.getDOMNode()).off('hidden.bs.modal', this.handleClose); }, getInitialState: function() { return { display_name: "", channel_name: "", channel_id: "" }; diff --git a/web/react/components/rename_team_modal.jsx b/web/react/components/rename_team_modal.jsx index a6da57b67..dfd775a3b 100644 --- a/web/react/components/rename_team_modal.jsx +++ b/web/react/components/rename_team_modal.jsx @@ -44,11 +44,14 @@ module.exports = React.createClass({ onNameChange: function() { this.setState({ name: this.refs.name.getDOMNode().value }) }, + handleClose: function() { + this.setState({ name: this.props.teamDisplayName, name_error: "", server_error: ""}); + }, componentDidMount: function() { - var self = this; - $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function(e) { - self.setState({ name: self.props.teamDisplayName }); - }); + $(this.refs.modal.getDOMNode()).on('hidden.bs.modal', this.handleClose); + }, + componentWillUnmount: function() { + $(this.refs.modal.getDOMNode()).off('hidden.bs.modal', this.handleClose); }, getInitialState: function() { return { name: this.props.teamDisplayName }; diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx index b8b667e1a..49eb58773 100644 --- a/web/react/components/setting_item_max.jsx +++ b/web/react/components/setting_item_max.jsx @@ -20,7 +20,7 @@ module.exports = React.createClass({ <hr /> { server_error } { client_error } - <a className="btn btn-sm btn-primary" onClick={this.props.submit}>Submit</a> + { this.props.submit ? <a className="btn btn-sm btn-primary" onClick={this.props.submit}>Submit</a> : "" } <a className="btn btn-sm theme" href="#" onClick={this.props.updateSection}>Cancel</a> </li> </ul> diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index 859e425a6..5b442aeac 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -6,6 +6,8 @@ var utils = require('../utils/utils.jsx'); var client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); +var Constants = require('../utils/constants.jsx'); + function getStateFromStores() { return { teams: UserStore.getTeams() }; } @@ -75,7 +77,7 @@ var NavbarDropdown = React.createClass({ <ul className="nav navbar-nav navbar-right"> <li className="dropdown"> <a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"> - <span className="dropdown__icon" dangerouslySetInnerHTML={{__html: " <svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>"}} /> + <span className="dropdown__icon" dangerouslySetInnerHTML={{__html: Constants.MENU_ICON }} /> </a> <ul className="dropdown-menu" role="menu"> <li><a href="#" data-toggle="modal" data-target="#user_settings1">Account Settings</a></li> diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index eed323d1f..bbf1f670c 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -46,7 +46,7 @@ module.exports = React.createClass({ function(data) { client.track('signup', 'signup_user_02_complete'); - client.loginByEmail(this.props.domain, this.state.user.email, this.state.user.password, + client.loginByEmail(this.props.teamName, this.state.user.email, this.state.user.password, function(data) { UserStore.setLastEmail(this.state.user.email); UserStore.setCurrentUser(data); @@ -58,7 +58,7 @@ module.exports = React.createClass({ }.bind(this), function(err) { if (err.message == "Login failed because email address has not been verified") { - window.location.href = "/verify_email?email="+ encodeURIComponent(this.state.user.email) + "&domain=" + encodeURIComponent(this.props.domain); + window.location.href = "/verify_email?email="+ encodeURIComponent(this.state.user.email) + "&domain=" + encodeURIComponent(this.props.teamName); } else { this.state.server_error = err.message; this.setState(this.state); @@ -79,7 +79,7 @@ module.exports = React.createClass({ props = {}; props.wizard = "welcome"; props.user = {}; - props.user.team_id = this.props.team_id; + props.user.team_id = this.props.teamId; props.user.email = this.props.email; props.hash = this.props.hash; props.data = this.props.data; @@ -103,7 +103,7 @@ module.exports = React.createClass({ var yourEmailIs = this.state.user.email == "" ? "" : <span>Your email address is { this.state.user.email }. </span> - var email = + var email = ( <div className={ this.state.original_email == "" ? "" : "hidden"} > <label className="control-label">Email</label> <div className={ email_error ? "form-group has-error" : "form-group" }> @@ -111,17 +111,30 @@ module.exports = React.createClass({ { email_error } </div> </div> + ); + + var auth_services = JSON.parse(this.props.authServices); + + var signup_message; + if (auth_services.indexOf("gitlab") >= 0) { + signup_message = <div><a className="btn btn-custom-login gitlab" href={"/"+this.props.teamName+"/signup/gitlab"+window.location.search}><span className="icon" />{"with GitLab"}</a> + <div className="or__container"><span>or</span></div></div>; + } return ( <div> <img className="signup-team-logo" src="/static/images/logo.png" /> - <h4>Welcome to { config.SiteName }</h4> - <p>{"Choose your username and password for the " + this.props.team_name + " " + strings.Team +"."}</p> - <p>Your username can be made of lowercase letters and numbers.</p> + <h3 className="text-center extra-margin">Signup to { config.SiteName }</h3> + <div className="form-group form-group--small"> + <span></span> + </div> + { signup_message } <label className="control-label">Username</label> <div className={ name_error ? "form-group has-error" : "form-group" }> <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" /> { name_error } + <p className="form__hint">Your username can be made of lowercase letters and numbers.</p> + <p className="form__hint">{"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others"}</p> </div> { email } <label className="control-label">Password</label> @@ -129,7 +142,6 @@ module.exports = React.createClass({ <input type="password" ref="password" className="form-control" placeholder="" maxLength="128" /> { password_error } </div> - <p>{"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others"}</p> <p className={ this.state.original_email == "" ? "hidden" : ""}>{ yourEmailIs } You’ll use this address to sign in to {config.SiteName}.</p> <div className="checkbox"><label><input type="checkbox" ref="email_service" /> It's ok to send me occassional email with updates about the {config.SiteName} service. </label></div> <p><button onClick={this.handleSubmit} className="btn-primary btn">Create Account</button></p> diff --git a/web/react/components/signup_user_oauth.jsx b/web/react/components/signup_user_oauth.jsx new file mode 100644 index 000000000..6322aedee --- /dev/null +++ b/web/react/components/signup_user_oauth.jsx @@ -0,0 +1,84 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + + +var utils = require('../utils/utils.jsx'); +var client = require('../utils/client.jsx'); +var UserStore = require('../stores/user_store.jsx'); +var BrowserStore = require('../stores/browser_store.jsx'); + +module.exports = React.createClass({ + handleSubmit: function(e) { + e.preventDefault(); + + if (!this.state.user.username) { + this.setState({name_error: "This field is required", email_error: "", password_error: "", server_error: ""}); + return; + } + + var username_error = utils.isValidUsername(this.state.user.username); + if (username_error === "Cannot use a reserved word as a username.") { + this.setState({name_error: "This username is reserved, please choose a new one.", email_error: "", password_error: "", server_error: ""}); + return; + } else if (username_error) { + this.setState({name_error: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'.", email_error: "", password_error: "", server_error: ""}); + return; + } + + this.setState({name_error: "", server_error: ""}); + + this.state.user.allow_marketing = this.refs.email_service.getDOMNode().checked; + + var user = this.state.user; + client.createUser(user, "", "", + function(data) { + client.track('signup', 'signup_user_oauth_02'); + window.location.href = '/' + this.props.teamName + '/login/'+user.auth_service; + }.bind(this), + function(err) { + this.state.server_error = err.message; + this.setState(this.state); + }.bind(this) + ); + }, + handleChange: function() { + var user = this.state.user; + user.username = this.refs.name.getDOMNode().value; + this.setState({ user: user }); + }, + getInitialState: function() { + var user = JSON.parse(this.props.user); + return { user: user }; + }, + render: function() { + + client.track('signup', 'signup_user_oauth_01'); + + var name_error = this.state.name_error ? <label className='control-label'>{ this.state.name_error }</label> : null; + var server_error = this.state.server_error ? <div className={ "form-group has-error" }><label className='control-label'>{ this.state.server_error }</label></div> : null; + + var yourEmailIs = this.state.user.email == "" ? "" : <span>Your email address is <b>{ this.state.user.email }.</b></span>; + + return ( + <div> + <img className="signup-team-logo" src="/static/images/logo.png" /> + <h4>Welcome to { config.SiteName }</h4> + <p>{"To continue signing up with " + this.state.user.auth_service + ", please register a username."}</p> + <p>Your username can be made of lowercase letters and numbers.</p> + <label className="control-label">Username</label> + <div className={ name_error ? "form-group has-error" : "form-group" }> + <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" value={this.state.user.username} onChange={this.handleChange} /> + { name_error } + </div> + <p>{"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others."}</p> + <p>{ yourEmailIs } You’ll use this address to sign in to {config.SiteName}.</p> + <div className="checkbox"><label><input type="checkbox" ref="email_service" /> It's ok to send me occassional email with updates about the {config.SiteName} service. </label></div> + <p><button onClick={this.handleSubmit} className="btn-primary btn">Create Account</button></p> + { server_error } + <p>By proceeding to create your account and use { config.SiteName }, you agree to our <a href={ config.TermsLink }>Terms of Service</a> and <a href={ config.PrivacyLink }>Privacy Policy</a>. If you do not agree, you cannot use {config.SiteName}.</p> + </div> + ); + } +}); + + diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx index 0f600a813..2ac9a2371 100644 --- a/web/react/components/user_settings.jsx +++ b/web/react/components/user_settings.jsx @@ -11,6 +11,7 @@ var client = require('../utils/client.jsx'); var AsyncClient = require('../utils/async_client.jsx'); var utils = require('../utils/utils.jsx'); var Constants = require('../utils/constants.jsx'); +var assign = require('object-assign'); function getNotificationsStateFromStores() { var user = UserStore.getCurrentUser(); @@ -95,11 +96,20 @@ var NotificationsTab = React.createClass({ }.bind(this) ); }, + handleClose: function() { + $(this.getDOMNode()).find(".form-control").each(function() { + this.value = ""; + }); + + this.setState(assign({},getNotificationsStateFromStores(),{server_error: null})); + }, componentDidMount: function() { UserStore.addChangeListener(this._onChange); + $('#user_settings1').on('hidden.bs.modal', this.handleClose); }, componentWillUnmount: function() { UserStore.removeChangeListener(this._onChange); + $('#user_settings1').off('hidden.bs.modal', this.handleClose); }, _onChange: function() { var newState = getNotificationsStateFromStores(); @@ -449,7 +459,7 @@ var SecurityTab = React.createClass({ submitPassword: function(e) { e.preventDefault(); - var user = UserStore.getCurrentUser(); + var user = this.props.user; var currentPassword = this.state.current_password; var newPassword = this.state.new_password; var confirmPassword = this.state.confirm_password; @@ -502,6 +512,18 @@ var SecurityTab = React.createClass({ handleDevicesOpen: function() { $("#user_settings1").modal('hide'); }, + handleClose: function() { + $(this.getDOMNode()).find(".form-control").each(function() { + this.value = ""; + }); + this.setState({current_password: '', new_password: '', confirm_password: '', server_error: null, password_error: null}); + }, + componentDidMount: function() { + $('#user_settings1').on('hidden.bs.modal', this.handleClose); + }, + componentWillUnmount: function() { + $('#user_settings1').off('hidden.bs.modal', this.handleClose); + }, getInitialState: function() { return { current_password: '', new_password: '', confirm_password: '' }; }, @@ -513,53 +535,69 @@ var SecurityTab = React.createClass({ var self = this; if (this.props.activeSection === 'password') { var inputs = []; + var submit = null; - inputs.push( - <div className="form-group"> - <label className="col-sm-5 control-label">Current Password</label> - <div className="col-sm-7"> - <input className="form-control" type="password" onChange={this.updateCurrentPassword} value={this.state.current_password}/> + if (this.props.user.auth_service === "") { + inputs.push( + <div className="form-group"> + <label className="col-sm-5 control-label">Current Password</label> + <div className="col-sm-7"> + <input className="form-control" type="password" onChange={this.updateCurrentPassword} value={this.state.current_password}/> + </div> </div> - </div> - ); - inputs.push( - <div className="form-group"> - <label className="col-sm-5 control-label">New Password</label> - <div className="col-sm-7"> - <input className="form-control" type="password" onChange={this.updateNewPassword} value={this.state.new_password}/> + ); + inputs.push( + <div className="form-group"> + <label className="col-sm-5 control-label">New Password</label> + <div className="col-sm-7"> + <input className="form-control" type="password" onChange={this.updateNewPassword} value={this.state.new_password}/> + </div> </div> - </div> - ); - inputs.push( - <div className="form-group"> - <label className="col-sm-5 control-label">Retype New Password</label> - <div className="col-sm-7"> - <input className="form-control" type="password" onChange={this.updateConfirmPassword} value={this.state.confirm_password}/> + ); + inputs.push( + <div className="form-group"> + <label className="col-sm-5 control-label">Retype New Password</label> + <div className="col-sm-7"> + <input className="form-control" type="password" onChange={this.updateConfirmPassword} value={this.state.confirm_password}/> + </div> </div> - </div> - ); + ); + + submit = this.submitPassword; + } else { + inputs.push( + <div className="form-group"> + <label className="col-sm-12">Log in occurs through GitLab. Please see your GitLab account settings page to update your password.</label> + </div> + ); + } passwordSection = ( <SettingItemMax title="Password" inputs={inputs} - submit={this.submitPassword} + submit={submit} server_error={server_error} client_error={password_error} updateSection={function(e){self.props.updateSection("");e.preventDefault();}} /> ); } else { - var d = new Date(this.props.user.last_password_update); - var hour = d.getHours() % 12 ? String(d.getHours() % 12) : "12"; - var min = d.getMinutes() < 10 ? "0" + d.getMinutes() : String(d.getMinutes()); - var timeOfDay = d.getHours() >= 12 ? " pm" : " am"; - var dateStr = "Last updated " + Constants.MONTHS[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear() + " at " + hour + ":" + min + timeOfDay; + var describe; + if (this.props.user.auth_service === "") { + var d = new Date(this.props.user.last_password_update); + var hour = d.getHours() % 12 ? String(d.getHours() % 12) : "12"; + var min = d.getMinutes() < 10 ? "0" + d.getMinutes() : String(d.getMinutes()); + var timeOfDay = d.getHours() >= 12 ? " pm" : " am"; + describe = "Last updated " + Constants.MONTHS[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear() + " at " + hour + ":" + min + timeOfDay; + } else { + describe = "Log in done through GitLab" + } passwordSection = ( <SettingItemMin title="Password" - describe={dateStr} + describe={describe} updateSection={function(){self.props.updateSection("password");}} /> ); @@ -737,6 +775,19 @@ var GeneralTab = React.createClass({ this.submitActive = false this.props.updateSection(section); }, + handleClose: function() { + $(this.getDOMNode()).find(".form-control").each(function() { + this.value = ""; + }); + + this.setState(assign({}, this.getInitialState(), {client_error: null, server_error: null, email_error: null})); + }, + componentDidMount: function() { + $('#user_settings1').on('hidden.bs.modal', this.handleClose); + }, + componentWillUnmount: function() { + $('#user_settings1').off('hidden.bs.modal', this.handleClose); + }, getInitialState: function() { var user = this.props.user; @@ -980,10 +1031,14 @@ var AppearanceTab = React.createClass({ var hex = utils.rgb2hex(e.target.style.backgroundColor); this.setState({ theme: hex.toLowerCase() }); }, + handleClose: function() { + this.setState({server_error: null}); + }, componentDidMount: function() { if (this.props.activeSection === "theme") { $(this.refs[this.state.theme].getDOMNode()).addClass('active-border'); } + $('#user_settings1').on('hidden.bs.modal', this.handleClose); }, componentDidUpdate: function() { if (this.props.activeSection === "theme") { @@ -991,6 +1046,9 @@ var AppearanceTab = React.createClass({ $(this.refs[this.state.theme].getDOMNode()).addClass('active-border'); } }, + componentWillUnmount: function() { + $('#user_settings1').off('hidden.bs.modal', this.handleClose); + }, getInitialState: function() { var user = UserStore.getCurrentUser(); var theme = config.ThemeColors != null ? config.ThemeColors[0] : "#2389d7"; diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx index 8348f0b5d..6e7528373 100644 --- a/web/react/pages/login.jsx +++ b/web/react/pages/login.jsx @@ -3,9 +3,9 @@ var Login = require('../components/login.jsx'); -global.window.setup_login_page = function(teamDisplayName, teamName) { +global.window.setup_login_page = function(team_display_name, team_name, auth_services) { React.render( - <Login teamDisplayName={teamDisplayName} teamName={teamName}/>, + <Login teamDisplayName={team_display_name} teamName={team_name} authServices={auth_services} />, document.getElementById('login') ); }; diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx index a24c8d4c8..60c3a609a 100644 --- a/web/react/pages/signup_user_complete.jsx +++ b/web/react/pages/signup_user_complete.jsx @@ -3,9 +3,9 @@ var SignupUserComplete =require('../components/signup_user_complete.jsx'); -global.window.setup_signup_user_complete_page = function(email, domain, name, id, data, hash) { +global.window.setup_signup_user_complete_page = function(email, name, ui_name, id, data, hash, auth_services) { React.render( - <SignupUserComplete team_id={id} domain={domain} team_name={name} email={email} hash={hash} data={data} />, + <SignupUserComplete teamId={id} teamName={name} teamDisplayName={ui_name} email={email} hash={hash} data={data} authServices={auth_services} />, document.getElementById('signup-user-complete') ); -};
\ No newline at end of file +}; diff --git a/web/react/pages/signup_user_oauth.jsx b/web/react/pages/signup_user_oauth.jsx new file mode 100644 index 000000000..6a0707702 --- /dev/null +++ b/web/react/pages/signup_user_oauth.jsx @@ -0,0 +1,11 @@ +// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. +// See License.txt for license information. + +var SignupUserOAuth = require('../components/signup_user_oauth.jsx'); + +global.window.setup_signup_user_oauth_page = function(user, team_name, team_display_name) { + React.render( + <SignupUserOAuth user={user} teamName={team_name} teamDisplayName={team_display_name} />, + document.getElementById('signup-user-complete') + ); +}; diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx index 2249da0d3..77ce19530 100644 --- a/web/react/utils/constants.jsx +++ b/web/react/utils/constants.jsx @@ -82,5 +82,7 @@ module.exports = { MONTHS: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], MAX_DMS: 10, ONLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path class='online--icon' d='M6,5.487c1.371,0,2.482-1.116,2.482-2.493c0-1.378-1.111-2.495-2.482-2.495S3.518,1.616,3.518,2.994C3.518,4.371,4.629,5.487,6,5.487z M10.452,8.545c-0.101-0.829-0.36-1.968-0.726-2.541C9.475,5.606,8.5,5.5,8.5,5.5S8.43,7.521,6,7.521C3.507,7.521,3.5,5.5,3.5,5.5S2.527,5.606,2.273,6.004C1.908,6.577,1.648,7.716,1.547,8.545C1.521,8.688,1.49,9.082,1.498,9.142c0.161,1.295,2.238,2.322,4.375,2.358C5.916,11.501,5.958,11.501,6,11.501c0.043,0,0.084,0,0.127-0.001c2.076-0.026,4.214-1.063,4.375-2.358C10.509,9.082,10.471,8.696,10.452,8.545z'/></g></g></svg>", - OFFLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path fill='#cccccc' d='M6.002,7.143C5.645,7.363,5.167,7.52,4.502,7.52c-2.493,0-2.5-2.02-2.5-2.02S1.029,5.607,0.775,6.004C0.41,6.577,0.15,7.716,0.049,8.545c-0.025,0.145-0.057,0.537-0.05,0.598c0.162,1.295,2.237,2.321,4.375,2.357c0.043,0.001,0.085,0.001,0.127,0.001c0.043,0,0.084,0,0.127-0.001c1.879-0.023,3.793-0.879,4.263-2h-2.89L6.002,7.143L6.002,7.143z M4.501,5.488c1.372,0,2.483-1.117,2.483-2.494c0-1.378-1.111-2.495-2.483-2.495c-1.371,0-2.481,1.117-2.481,2.495C2.02,4.371,3.13,5.488,4.501,5.488z M7.002,6.5v2h5v-2H7.002z'/></g></g></svg>" + OFFLINE_ICON_SVG: "<svg version='1.1' id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:cc='http://creativecommons.org/ns#' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' sodipodi:docname='TRASH_1_4.svg' inkscape:version='0.48.4 r9939' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='12px' height='12px' viewBox='0 0 12 12' enable-background='new 0 0 12 12' xml:space='preserve'><sodipodi:namedview inkscape:cy='139.7898' inkscape:cx='26.358185' inkscape:zoom='1.18' showguides='true' showgrid='false' id='namedview6' guidetolerance='10' gridtolerance='10' objecttolerance='10' borderopacity='1' bordercolor='#666666' pagecolor='#ffffff' inkscape:current-layer='Layer_1' inkscape:window-maximized='1' inkscape:window-y='-8' inkscape:window-x='-8' inkscape:window-height='705' inkscape:window-width='1366' inkscape:guide-bbox='true' inkscape:pageshadow='2' inkscape:pageopacity='0'><sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide><sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide></sodipodi:namedview><g><g><path fill='#cccccc' d='M6.002,7.143C5.645,7.363,5.167,7.52,4.502,7.52c-2.493,0-2.5-2.02-2.5-2.02S1.029,5.607,0.775,6.004C0.41,6.577,0.15,7.716,0.049,8.545c-0.025,0.145-0.057,0.537-0.05,0.598c0.162,1.295,2.237,2.321,4.375,2.357c0.043,0.001,0.085,0.001,0.127,0.001c0.043,0,0.084,0,0.127-0.001c1.879-0.023,3.793-0.879,4.263-2h-2.89L6.002,7.143L6.002,7.143z M4.501,5.488c1.372,0,2.483-1.117,2.483-2.494c0-1.378-1.111-2.495-2.483-2.495c-1.371,0-2.481,1.117-2.481,2.495C2.02,4.371,3.13,5.488,4.501,5.488z M7.002,6.5v2h5v-2H7.002z'/></g></g></svg>", + MENU_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>", + COMMENT_ICON: "<svg version='1.1' id='Layer_2' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='15px' height='15px' viewBox='1 1.5 15 15' enable-background='new 1 1.5 15 15' xml:space='preserve'> <g> <g> <path fill='#211B1B' d='M14,1.5H3c-1.104,0-2,0.896-2,2v8c0,1.104,0.896,2,2,2h1.628l1.884,3l1.866-3H14c1.104,0,2-0.896,2-2v-8 C16,2.396,15.104,1.5,14,1.5z M15,11.5c0,0.553-0.447,1-1,1H8l-1.493,2l-1.504-1.991L5,12.5H3c-0.552,0-1-0.447-1-1v-8 c0-0.552,0.448-1,1-1h11c0.553,0,1,0.448,1,1V11.5z'/> </g> </g> </svg>" }; diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index 4351e167b..687e330a6 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -96,6 +96,10 @@ right: 22px; .dropdown-toggle { padding: 10px; + @include opacity(0.8); + &:hover { + @include opacity(1); + } } .dropdown-menu { li a { @@ -119,7 +123,7 @@ } .header__info { color: #fff; - padding-left: 3px + padding-left: 4px; } .team__name, .user__name { display: block; diff --git a/web/sass-files/sass/partials/_navbar.scss b/web/sass-files/sass/partials/_navbar.scss index 6d8f11ce3..e5e67a9e0 100644 --- a/web/sass-files/sass/partials/_navbar.scss +++ b/web/sass-files/sass/partials/_navbar.scss @@ -24,9 +24,10 @@ border-radius: 0; margin: 0; padding: 0 10px; - line-height: 50px; + line-height: 53px; height: 50px; z-index: 5; + fill: #fff; .icon-bar { background: #fff; width: 21px; diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index 3a2768a47..2d78cf242 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -459,14 +459,15 @@ border-radius: 0; padding: 0; border-bottom: 1px solid #FFF; - border-bottom: 1px solid rgba(#fff, 0.6); - @include input-placeholder { - color: rgba(#fff, 0.6); + border-bottom: 1px solid rgba(#fff, 0.4); + &:focus { + border-bottom: 1px solid rgba(#fff, 0.8); } } input[type=text] { @include input-placeholder { color: #fff; + color: rgba(#fff, 0.6); } } } diff --git a/web/sass-files/sass/partials/_search.scss b/web/sass-files/sass/partials/_search.scss index d4a4da243..794358320 100644 --- a/web/sass-files/sass/partials/_search.scss +++ b/web/sass-files/sass/partials/_search.scss @@ -5,9 +5,10 @@ width: auto; height: auto; position: absolute; - top: 17px; + top: 1px; right: 15px; cursor: pointer; + padding: 1em 0; z-index: 5; display: none; } diff --git a/web/sass-files/sass/partials/_signup.scss b/web/sass-files/sass/partials/_signup.scss index db22718d2..826394a10 100644 --- a/web/sass-files/sass/partials/_signup.scss +++ b/web/sass-files/sass/partials/_signup.scss @@ -23,6 +23,9 @@ font-weight: 600; margin: 0 0 1.3em 0; font-size: 1.4em; + &.extra-margin { + margin-bottom: 2.5em; + } } h4 { font-size: em(18px); @@ -44,6 +47,11 @@ form { margin-bottom: 0.8em; } + .form__hint { + font-size: 0.95em; + color: #999; + margin: 10px 0; + } .external-link { position: absolute; bottom: 0; @@ -61,10 +69,53 @@ .form-control { height: em(38px); } + .or__container { + height: 1px; + background: #dddddd; + text-align: center; + margin: 2em 0; + span { + width: 33px; + top: -10px; + position: relative; + line-height: 20px; + font-weight: 600; + background: #fff; + display: inline-block; + } + } .btn { padding: em(7px) em(15px); font-weight: 600; font-size: em(13px); + &.btn-custom-login { + display: block; + min-width: 200px; + width: 200px; + padding: 0 1em; + margin: 1em auto; + height: 40px; + line-height: 35px; + color: #fff; + @include border-radius(2px); + &.gitlab { + background: #554488; + &:hover { + background: darken(#554488, 10%); + } + span { + vertical-align: middle; + } + .icon { + background: url("../images/gitlabLogo.png"); + width: 18px; + height: 18px; + margin-right: 8px; + @include background-size(100% 100%); + display: inline-block; + } + } + } &.btn-default { color: #444; } @@ -90,9 +141,20 @@ } .has-error { .control-label { - margin-top: 5px; + background: #f2f2f2; + padding: 0.7em 1em; + @include border-radius(3px); + margin: 1em 0 0; font-size: 14px; - font-weight: 600; + font-weight: normal; + color: #999; + width: 100%; + &:before { + @extend .fa; + content: "\f071"; + margin-right: 4px; + color: #aaa; + } } } .reset-form { diff --git a/web/static/images/gitlabLogo.png b/web/static/images/gitlabLogo.png Binary files differnew file mode 100644 index 000000000..9004a8f0c --- /dev/null +++ b/web/static/images/gitlabLogo.png diff --git a/web/templates/login.html b/web/templates/login.html index 24cebec8f..4b2813358 100644 --- a/web/templates/login.html +++ b/web/templates/login.html @@ -20,7 +20,7 @@ </div> </div> <script> -window.setup_login_page({{.Props.TeamDisplayName}}, {{.Props.TeamName}}); +window.setup_login_page('{{.Props.TeamDisplayName}}', '{{.Props.TeamName}}', '{{.Props.AuthServices}}'); </script> </body> </html> diff --git a/web/templates/signup_user_complete.html b/web/templates/signup_user_complete.html index 0cc655b63..176ca77b1 100644 --- a/web/templates/signup_user_complete.html +++ b/web/templates/signup_user_complete.html @@ -19,7 +19,7 @@ </div> </div> <script> - window.setup_signup_user_complete_page('{{.Props.Email}}', '{{.Props.TeamName}}', '{{.Props.TeamDisplayName}}', '{{.Props.TeamId}}', '{{.Props.Data}}', '{{.Props.Hash}}'); + window.setup_signup_user_complete_page('{{.Props.Email}}', '{{.Props.TeamName}}', '{{.Props.TeamDisplayName}}', '{{.Props.TeamId}}', '{{.Props.Data}}', '{{.Props.Hash}}', '{{.Props.AuthServices}}'); </script> </body> </html> diff --git a/web/templates/signup_user_oauth.html b/web/templates/signup_user_oauth.html new file mode 100644 index 000000000..2eddb50d2 --- /dev/null +++ b/web/templates/signup_user_oauth.html @@ -0,0 +1,26 @@ +{{define "signup_user_oauth"}} +<!DOCTYPE html> +<html> +{{template "head" . }} +<body class="white"> + <div class="container-fluid"> + <div class="inner__wrap"> + <div class="row content"> + <div class="col-sm-12"> + <div class="signup-team__container"> + <div id="signup-user-complete"></div> + </div> + </div> + <div class="footer-push"></div> + </div> + <div class="row footer"> + {{template "footer" . }} + </div> + </div> + </div> + <script> + window.setup_signup_user_oauth_page('{{.Props.User}}', '{{.Props.TeamName}}', '{{.Props.TeamDisplayName}}'); + </script> +</body> +</html> +{{end}} diff --git a/web/web.go b/web/web.go index 3e4bc2d53..1d59ef946 100644 --- a/web/web.go +++ b/web/web.go @@ -52,6 +52,11 @@ func InitWeb() { mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET") + + // Bug in gorilla.mux pervents us from using regex here. + mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") + mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(loginCompleteOAuth)).Methods("GET") + mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") // Bug in gorilla.mux pervents us from using regex here. @@ -61,6 +66,11 @@ func InitWeb() { mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET") mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET") mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET") + + // Bug in gorilla.mux pervents us from using regex here. + mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET") + mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(signupCompleteOAuth)).Methods("GET") + mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET") mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET") @@ -178,6 +188,7 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) { page := NewHtmlTemplatePage("login", "Login") page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamName"] = teamName + page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices()) page.Render(c, w) } @@ -264,6 +275,7 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) page.Props["TeamId"] = props["id"] page.Props["Data"] = data page.Props["Hash"] = hash + page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices()) page.Render(c, w) } @@ -439,3 +451,189 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { page.Props["IsReset"] = strconv.FormatBool(isResetLink) page.Render(c, w) } + +func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + teamName := params["team"] + + if len(teamName) == 0 { + c.Err = model.NewAppError("signupWithOAuth", "Invalid team name", "team_name="+teamName) + c.Err.StatusCode = http.StatusBadRequest + return + } + + hash := r.URL.Query().Get("h") + + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + if api.IsVerifyHashRequired(nil, team, hash) { + data := r.URL.Query().Get("d") + props := model.MapFromJson(strings.NewReader(data)) + + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) { + c.Err = model.NewAppError("signupWithOAuth", "The signup link does not appear to be valid", "") + return + } + + t, err := strconv.ParseInt(props["time"], 10, 64) + if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours + c.Err = model.NewAppError("signupWithOAuth", "The signup link has expired", "") + return + } + + if team.Id != props["id"] { + c.Err = model.NewAppError("signupWithOAuth", "Invalid team name", data) + return + } + } + + redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" + + api.GetAuthorizationCode(c, w, r, teamName, service, redirectUri) +} + +func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + teamName := r.FormValue("team") + + uri := c.GetSiteURL() + "/signup/" + service + "/complete?team=" + teamName + + if len(teamName) == 0 { + c.Err = model.NewAppError("signupCompleteOAuth", "Invalid team name", "team_name="+teamName) + c.Err.StatusCode = http.StatusBadRequest + return + } + + // Make sure team exists + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + if body, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil { + c.Err = err + return + } else { + var user *model.User + if service == model.USER_AUTH_SERVICE_GITLAB { + glu := model.GitLabUserFromJson(body) + user = model.UserFromGitLabUser(glu) + } + + if user == nil { + c.Err = model.NewAppError("signupCompleteOAuth", "Could not create user out of "+service+" user object", "") + return + } + + if result := <-api.Srv.Store.User().GetByAuth(team.Id, user.AuthData, service); result.Err == nil { + c.Err = model.NewAppError("signupCompleteOAuth", "This "+service+" account has already been used to sign up for team "+team.DisplayName, "email="+user.Email) + return + } + + if result := <-api.Srv.Store.User().GetByEmail(team.Id, user.Email); result.Err == nil { + c.Err = model.NewAppError("signupCompleteOAuth", "Team "+team.DisplayName+" already has a user with the email address attached to your "+service+" account", "email="+user.Email) + return + } + + user.TeamId = team.Id + + page := NewHtmlTemplatePage("signup_user_oauth", "Complete User Sign Up") + page.Props["User"] = user.ToJson() + page.Props["TeamName"] = team.Name + page.Props["TeamDisplayName"] = team.DisplayName + page.Render(c, w) + } +} + +func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + teamName := params["team"] + + if len(teamName) == 0 { + c.Err = model.NewAppError("loginWithOAuth", "Invalid team name", "team_name="+teamName) + c.Err.StatusCode = http.StatusBadRequest + return + } + + // Make sure team exists + if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } + + redirectUri := c.GetSiteURL() + "/login/" + service + "/complete" + + api.GetAuthorizationCode(c, w, r, teamName, service, redirectUri) +} + +func loginCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] + + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + teamName := r.FormValue("team") + + uri := c.GetSiteURL() + "/login/" + service + "/complete?team=" + teamName + + if len(teamName) == 0 { + c.Err = model.NewAppError("loginCompleteOAuth", "Invalid team name", "team_name="+teamName) + c.Err.StatusCode = http.StatusBadRequest + return + } + + // Make sure team exists + var team *model.Team + if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + if body, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil { + c.Err = err + return + } else { + authData := "" + if service == model.USER_AUTH_SERVICE_GITLAB { + glu := model.GitLabUserFromJson(body) + authData = glu.GetAuthData() + } + + if len(authData) == 0 { + c.Err = model.NewAppError("loginCompleteOAuth", "Could not parse auth data out of "+service+" user object", "") + return + } + + var user *model.User + if result := <-api.Srv.Store.User().GetByAuth(team.Id, authData, service); result.Err != nil { + c.Err = result.Err + return + } else { + user = result.Data.(*model.User) + api.Login(c, w, r, user, "") + + if c.Err != nil { + return + } + + root(c, w, r) + } + } +} |