diff options
Diffstat (limited to 'web')
33 files changed, 534 insertions, 167 deletions
diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index b23b3213f..462f046f6 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -64,7 +64,7 @@ module.exports = React.createClass({ <div>{"URL: " + currentAudit.action.replace("/api/v1", "")}</div> </div> : - <a href="#" onClick={this.handleMoreInfo.bind(this, i)}>More info</a> + <a href="#" className="theme" onClick={this.handleMoreInfo.bind(this, i)}>More info</a> } </div> {i < this.state.audits.length - 1 ? diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index d6f8f40eb..7cce807a9 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -68,6 +68,9 @@ module.exports = React.createClass({ else if (currentSession.props.platform === "Macintosh" || currentSession.props.platform === "iPhone") { devicePicture = "fa fa-apple"; } + else if (currentSession.props.platform === "Linux") { + devicePicture = "fa fa-linux"; + } activityList[i] = ( <div className="activity-log__table"> @@ -83,7 +86,7 @@ module.exports = React.createClass({ <div>{"Session ID: " + currentSession.alt_id}</div> </div> : - <a href="#" onClick={this.handleMoreInfo.bind(this, i)}>More info</a> + <a className="theme" href="#" onClick={this.handleMoreInfo.bind(this, i)}>More info</a> } </div> </div> diff --git a/web/react/components/command_list.jsx b/web/react/components/command_list.jsx index 023f5f760..5efe98dc6 100644 --- a/web/react/components/command_list.jsx +++ b/web/react/components/command_list.jsx @@ -20,12 +20,7 @@ module.exports = React.createClass({ }, getSuggestedCommands: function(cmd) { - if (cmd == "") { - this.setState({ suggestions: [ ], cmd: "" }); - return; - } - - if (cmd.indexOf("/") != 0) { + if (!cmd || cmd.charAt(0) != '/') { this.setState({ suggestions: [ ], cmd: "" }); return; } @@ -35,17 +30,19 @@ module.exports = React.createClass({ cmd, true, function(data) { - if (data.suggestions.length === 1 && data.suggestions[0].suggestion === cmd) data.suggestions = []; + if (data.suggestions.length === 1 && data.suggestions[0].suggestion === cmd) { + data.suggestions = []; + } this.setState({ suggestions: data.suggestions, cmd: cmd }); }.bind(this), function(err){ - }.bind(this) + } ); }, render: function() { if (this.state.suggestions.length == 0) return (<div/>); - var suggestions = [] + var suggestions = []; for (var i = 0; i < this.state.suggestions.length; i++) { if (this.state.suggestions[i].suggestion != this.state.cmd) { @@ -59,7 +56,7 @@ module.exports = React.createClass({ } return ( - <div ref="mentionlist" className="command-box" style={{height:(this.state.suggestions*37)+2}}> + <div ref="mentionlist" className="command-box" style={{height:(this.state.suggestions.length*37)+2}}> { suggestions } </div> ); diff --git a/web/react/components/error_bar.jsx b/web/react/components/error_bar.jsx index d9d91ef51..f7514a009 100644 --- a/web/react/components/error_bar.jsx +++ b/web/react/components/error_bar.jsx @@ -8,21 +8,25 @@ var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; function getStateFromStores() { - var error = ErrorStore.getLastError(); - if (error && error.message !== "There appears to be a problem with your internet connection") { - return { message: error.message }; - } else { - return { message: null }; - } + var error = ErrorStore.getLastError(); + if (error && error.message !== "There appears to be a problem with your internet connection") { + return { message: error.message }; + } else { + return { message: null }; + } } module.exports = React.createClass({ + displayName: 'ErrorBar', + componentDidMount: function() { ErrorStore.addChangeListener(this._onChange); - $('body').css('padding-top', $('#error_bar').outerHeight()); - $(window).resize(function(){ - $('body').css('padding-top', $('#error_bar').outerHeight()); - }); + $('body').css('padding-top', $(React.findDOMNode(this)).outerHeight()); + $(window).resize(function() { + if (this.state.message) { + $('body').css('padding-top', $(React.findDOMNode(this)).outerHeight()); + } + }.bind(this)); }, componentWillUnmount: function() { ErrorStore.removeChangeListener(this._onChange); @@ -31,39 +35,39 @@ module.exports = React.createClass({ var newState = getStateFromStores(); if (!utils.areStatesEqual(newState, this.state)) { if (newState.message) { - var self = this; - setTimeout(function(){self.handleClose();}, 10000); + setTimeout(this.handleClose, 10000); } + this.setState(newState); } }, handleClose: function(e) { if (e) e.preventDefault(); + AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_ERROR, err: null }); + $('body').css('padding-top', '0'); }, getInitialState: function() { var state = getStateFromStores(); if (state.message) { - var self = this; - setTimeout(function(){self.handleClose();}, 10000); + setTimeout(this.handleClose, 10000); } return state; }, render: function() { - var message = this.state.message; - if (message) { + if (this.state.message) { return ( <div className="error-bar"> - <span className="error-text">{message}</span> - <a href="#" className="error-close pull-right" onClick={this.handleClose}>×</a> + <span>{this.state.message}</span> + <a href="#" className="error-bar__close" onClick={this.handleClose}>×</a> </div> ); } else { return <div/>; } } -}); +});
\ No newline at end of file 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/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/setting_picture.jsx b/web/react/components/setting_picture.jsx index 62c889b7f..6cfb74d60 100644 --- a/web/react/components/setting_picture.jsx +++ b/web/react/components/setting_picture.jsx @@ -25,9 +25,9 @@ module.exports = React.createClass({ var img = null; if (this.props.picture) { - img = (<img ref="image" className="col-xs-5 profile-img" src=""/>); + img = (<img ref="image" className="profile-img" src=""/>); } else { - img = (<img ref="image" className="col-xs-5 profile-img" src={this.props.src}/>); + img = (<img ref="image" className="profile-img" src={this.props.src}/>); } var self = this; @@ -37,7 +37,7 @@ module.exports = React.createClass({ <li className="col-xs-12 section-title">{this.props.title}</li> <li className="col-xs-offset-3 col-xs-8"> <ul className="setting-list"> - <li className="row setting-list-item"> + <li className="setting-list-item"> {img} </li> <li className="setting-list-item"> diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index 7a7e92854..859e425a6 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -101,13 +101,13 @@ module.exports = React.createClass({ getDefaultProps: function() { return { - teamName: config.SiteName + teamDisplayName: config.SiteName }; }, render: function() { - var teamDisplayName = this.props.teamDisplayName ? this.props.teamDisplayName : config.SiteName; - var me = UserStore.getCurrentUser() + var me = UserStore.getCurrentUser(); + if (!me) { return null; } @@ -118,11 +118,11 @@ module.exports = React.createClass({ { me.last_picture_update ? <img className="user__picture" src={"/api/v1/users/" + me.id + "/image?time=" + me.update_at} /> : - <div /> + null } <div className="header__info"> <div className="user__name">{ '@' + me.username}</div> - <div className="team__name">{ teamDisplayName }</div> + <div className="team__name">{ this.props.teamDisplayName }</div> </div> </a> <NavbarDropdown teamType={this.props.teamType} /> diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index cf982cc1e..362f79163 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -69,7 +69,9 @@ module.exports = React.createClass({ { name_error } </div> { server_error } - <button className="btn btn-md btn-primary" type="submit">Sign up for Free</button> + <div className="form-group"> + <button className="btn btn-md btn-primary" type="submit">Sign up for Free</button> + </div> <div className="form-group form-group--small"> <span><a href="/find_team">{"Find my " + strings.Team}</a></span> </div> diff --git a/web/react/components/signup_team_complete.jsx b/web/react/components/signup_team_complete.jsx index 9ceeb6324..3e8a57308 100644 --- a/web/react/components/signup_team_complete.jsx +++ b/web/react/components/signup_team_complete.jsx @@ -246,7 +246,7 @@ TeamURLPage = React.createClass({ <h2>{utils.toTitleCase(strings.Team) + " URL"}</h2> <div className={ name_error ? "form-group has-error" : "form-group" }> <div className="row"> - <div className="col-sm-9"> + <div className="col-sm-11"> <div className="input-group"> <span className="input-group-addon">{ window.location.origin + "/" }</span> <input type="text" ref="name" className="form-control" placeholder="" maxLength="128" defaultValue={this.props.state.team.name} autoFocus={true} onFocus={this.handleFocus}/> diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index eed323d1f..dc5ba64aa 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,12 +111,25 @@ 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 = <p>{"Choose your username and password for the " + this.props.teamDisplayName + " " + strings.Team} <a href={"/"+this.props.teamName+"/signup/gitlab"+window.location.search}>{"or sign up with GitLab."}</a></p>; + } else { + signup_message = <p>{"Choose your username and password for the " + this.props.teamDisplayName + " " + strings.Team + "."}</p>; + } 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> + <div className="form-group form-group--small"> + <span></span> + </div> + { signup_message } <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" }> 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/team_settings.jsx b/web/react/components/team_settings.jsx index 166b1f38b..3bbb5e892 100644 --- a/web/react/components/team_settings.jsx +++ b/web/react/components/team_settings.jsx @@ -73,13 +73,12 @@ var FeatureTab = React.createClass({ var inputs = []; inputs.push( - <div className="col-sm-12"> + <div> <div className="btn-group" data-toggle="buttons-radio"> <button className={"btn btn-default "+valetActive[0]} onClick={function(){self.handleValetRadio("true")}}>On</button> <button className={"btn btn-default "+valetActive[1]} onClick={function(){self.handleValetRadio("false")}}>Off</button> </div> <div><br/>Valet is a preview feature for enabling a non-user account limited to basic member permissions that can be manipulated by 3rd parties.<br/><br/>IMPORTANT: The preview version of Valet should not be used without a secure connection and a trusted 3rd party, since user credentials are used to connect. OAuth2 will be used in the final release.</div> - <br></br> </div> ); diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx index ad890334e..298f5ee70 100644 --- a/web/react/components/user_settings.jsx +++ b/web/react/components/user_settings.jsx @@ -449,7 +449,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; @@ -513,53 +513,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");}} /> ); @@ -577,9 +593,9 @@ var SecurityTab = React.createClass({ { passwordSection } <div className="divider-dark"/> <br></br> - <a data-toggle="modal" className="security-links" data-target="#access-history" href="#" onClick={this.handleHistoryOpen}><i className="fa fa-clock-o"></i>View Access History</a> + <a data-toggle="modal" className="security-links theme" data-target="#access-history" href="#" onClick={this.handleHistoryOpen}><i className="fa fa-clock-o"></i>View Access History</a> <b> </b> - <a data-toggle="modal" className="security-links" data-target="#activity-log" href="#" onClick={this.handleDevicesOpen}><i className="fa fa-globe"></i>View and Logout of Active Devices</a> + <a data-toggle="modal" className="security-links theme" data-target="#activity-log" href="#" onClick={this.handleDevicesOpen}><i className="fa fa-globe"></i>View and Logout of Active Devices</a> </div> </div> ); 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/stores/user_store.jsx b/web/react/stores/user_store.jsx index d03016c5d..001162f47 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -183,6 +183,9 @@ var UserStore = assign({}, EventEmitter.prototype, { var keys = []; + if (!user) + return keys; + if (user.notify_props && user.notify_props.mention_keys) keys = keys.concat(user.notify_props.mention_keys.split(',')); if (user.first_name && user.notify_props.first_name === "true") keys.push(user.first_name); if (user.notify_props.all === "true") keys.push('@all'); @@ -258,4 +261,3 @@ UserStore.dispatchToken = AppDispatcher.register(function(payload) { UserStore.setMaxListeners(0); global.window.UserStore = UserStore; module.exports = UserStore; - diff --git a/web/sass-files/sass/partials/_access-history.scss b/web/sass-files/sass/partials/_access-history.scss index f54c9a122..412a2a1d0 100644 --- a/web/sass-files/sass/partials/_access-history.scss +++ b/web/sass-files/sass/partials/_access-history.scss @@ -12,7 +12,7 @@ } .access__date { font-weight: 600; - font-size: 16px; + font-size: 15px; width: 190px; } .access__report { @@ -21,7 +21,7 @@ } .report__time { font-weight: 600; - font-size: 16px; + font-size: 15px; } .report__info { color: #999; diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index 1fb970075..52659521d 100644 --- a/web/sass-files/sass/partials/_base.scss +++ b/web/sass-files/sass/partials/_base.scss @@ -5,7 +5,7 @@ html, body { body { font-family: 'Open Sans', sans-serif; -webkit-font-smoothing: antialiased; - background: #e9e9e9; + background: $body-bg; position: relative; height: 100%; &.white { @@ -96,32 +96,6 @@ div.theme { position:relative; } -.command-box { - position:absolute; - background-color:#fff; - width:100%; - border:1px solid #ddd; - bottom: 38; -} - -.command-name { - position: relative; - width: 100%; - background-color: #fff; - height: 37px; - line-height: 37px; - padding: 2px 10px 2px 5px; - z-index: 101; -} - -.command-name:hover { - background-color:#e8eaed; -} - -.command-desc { - color: #a7a8ab; -} - @-webkit-keyframes spin2 { from { -webkit-transform: rotate(0deg);} to { -webkit-transform: rotate(360deg);} @@ -139,29 +113,3 @@ div.theme { .black-bg { background-color: black !important; } - -#error_bar { - background-color: #0099FF; - text-align:center; - position: relative; - color: #fff; - position: fixed; - top: 0; - width: 100%; - z-index: 9999; - - .error-bar { - padding: 5px 30px; - } - - .error-close { - position: absolute; - right: 0; - top: 0; - color: #FFF; - font-size: 20px; - font-weight: 600; - text-decoration: none; - padding: 0 10px; - } -}
\ No newline at end of file diff --git a/web/sass-files/sass/partials/_command-box.scss b/web/sass-files/sass/partials/_command-box.scss new file mode 100644 index 000000000..44eb9b8df --- /dev/null +++ b/web/sass-files/sass/partials/_command-box.scss @@ -0,0 +1,25 @@ +.command-box { + position: absolute; + background-color: #fff; + width: 100%; + border: $border-gray; + bottom: 38px; + @extend %popover-box-shadow; +} + +.command-name { + position: relative; + width: 100%; + background-color: #fff; + height: 37px; + line-height: 37px; + padding: 2px 10px 2px 5px; + z-index: 101; + &:hover { + background-color: #e8eaed; + } +} + +.command-desc { + color: #a7a8ab; +}
\ No newline at end of file diff --git a/web/sass-files/sass/partials/_error-bar.scss b/web/sass-files/sass/partials/_error-bar.scss new file mode 100644 index 000000000..2e3d3c87e --- /dev/null +++ b/web/sass-files/sass/partials/_error-bar.scss @@ -0,0 +1,25 @@ +.error-bar { + background-color: #0099FF; + text-align:center; + position: relative; + color: #fff; + position: fixed; + top: 0; + width: 100%; + z-index: 9999; + padding: 5px 30px; + &__close { + position: absolute; + right: 0; + top: 0; + color: #FFF; + font-size: 20px; + font-weight: 600; + text-decoration: none; + padding: 0 10px; + &:hover { + color: #FFF; + text-decoration: none; + } + } +} diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index eab4becac..4351e167b 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -92,10 +92,10 @@ .navbar-right { font-size: 0.85em; position: absolute; - top: 20px; + top: 10px; right: 22px; .dropdown-toggle { - padding: 0 10px; + padding: 10px; } .dropdown-menu { li a { @@ -119,6 +119,7 @@ } .header__info { color: #fff; + padding-left: 3px } .team__name, .user__name { display: block; diff --git a/web/sass-files/sass/partials/_mentions.scss b/web/sass-files/sass/partials/_mentions.scss index 1396f21a1..a8c4dec26 100644 --- a/web/sass-files/sass/partials/_mentions.scss +++ b/web/sass-files/sass/partials/_mentions.scss @@ -10,6 +10,7 @@ .mentions--top { position: absolute; z-index: 1060; + @extend %popover-box-shadow; .mentions-box { width: 100%; height: 100%; diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index d8a8fd982..3a2768a47 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -451,31 +451,23 @@ color: #fff; .search__form { border: none; - padding: 0 60px 0 25px; + padding: 0 60px 0 35px; .form-control { - line-height: 31px; + line-height: normal; background: none; color: #fff; border-radius: 0; - padding: 0 10px 0; + padding: 0; + border-bottom: 1px solid #FFF; + border-bottom: 1px solid rgba(#fff, 0.6); @include input-placeholder { color: rgba(#fff, 0.6); } } - ::-webkit-input-placeholder { - color: #fff; - } - - :-moz-placeholder { /* Firefox 18- */ - color: #fff; - } - - ::-moz-placeholder { /* Firefox 19+ */ - color: #fff; - } - - :-ms-input-placeholder { - color: #fff; + input[type=text] { + @include input-placeholder { + color: #fff; + } } } } diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index b8dc9e997..1fb078bb9 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -163,8 +163,10 @@ } .profile-img { - width:158px; - max-height:128px; + width:128px; + height:128px; + margin-bottom: 10px; + @include border-radius(128px); } .sel-btn { @@ -232,4 +234,4 @@ .color-btn { margin:4px; -}
\ No newline at end of file +} diff --git a/web/sass-files/sass/partials/_signup.scss b/web/sass-files/sass/partials/_signup.scss index 98931279b..db22718d2 100644 --- a/web/sass-files/sass/partials/_signup.scss +++ b/web/sass-files/sass/partials/_signup.scss @@ -6,7 +6,7 @@ } .signup-team__container { padding: 100px 0px 50px 0px; - max-width: 600px; + max-width: 380px; margin: 0 auto; font-size: 1.1em; position: relative; diff --git a/web/sass-files/sass/partials/_variables.scss b/web/sass-files/sass/partials/_variables.scss index 5d883ab44..78952abb5 100644 --- a/web/sass-files/sass/partials/_variables.scss +++ b/web/sass-files/sass/partials/_variables.scss @@ -7,4 +7,8 @@ $primary-color: #2389D7; $primary-color--hover: darken(#2389D7, 5%); $body-bg: #e9e9e9; $header-bg: #f9f9f9; -$border-gray: 1px solid #ddd;
\ No newline at end of file +$border-gray: 1px solid #ddd; + +%popover-box-shadow { + @include box-shadow(rgba(black, 0.175) 1px -3px 12px); +} diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss index 294f6122a..ffd1f42b8 100644 --- a/web/sass-files/sass/styles.scss +++ b/web/sass-files/sass/styles.scss @@ -29,7 +29,9 @@ @import "partials/settings"; @import "partials/modal"; @import "partials/mentions"; +@import "partials/command-box"; @import "partials/error"; +@import "partials/error-bar"; @import "partials/loading"; // Responsive Css 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) + } + } +} |