add oauth flows, profile, logout page, and index page

This commit is contained in:
cleverbeagle 2017-05-29 22:02:22 -05:00
parent 650c93273a
commit b0270cc98b
31 changed files with 449 additions and 162 deletions

View file

@ -46,6 +46,6 @@ Documents.schema = new SimpleSchema({
},
});
// Documents.attachSchema(Documents.schema);
Documents.attachSchema(Documents.schema);
export default Documents;

View file

@ -0,0 +1,14 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { ServiceConfiguration } from 'meteor/service-configuration';
Meteor.methods({
'oauth.verifyConfiguration': function oauthVerifyConfiguration(services) {
check(services, Array);
const verifiedServices = [];
services.forEach((service) => {
if (ServiceConfiguration.configurations.findOne({ service })) verifiedServices.push(service);
});
return verifiedServices.sort();
},
});

View file

@ -5,6 +5,7 @@ Meteor.publish('users.editProfile', function usersProfile() {
fields: {
emails: 1,
profile: 1,
services: 1,
},
});
});

View file

@ -0,0 +1,2 @@
import './oauth';
import './email-templates';

View file

@ -0,0 +1,13 @@
import { Meteor } from 'meteor/meteor';
import { ServiceConfiguration } from 'meteor/service-configuration';
const OAuthSettings = Meteor.settings.private.OAuth;
if (OAuthSettings) {
Object.keys(OAuthSettings).forEach((service) => {
ServiceConfiguration.configurations.upsert(
{ service },
{ $set: OAuthSettings[service] },
);
});
}

View file

@ -0,0 +1,7 @@
import { Accounts } from 'meteor/accounts-base';
Accounts.onCreateUser((options, user) => {
const userToCreate = user;
if (options.profile) userToCreate.profile = options.profile;
return userToCreate;
});

View file

@ -1,5 +1,7 @@
import '../../api/Documents/methods';
import '../../api/Documents/server/publications';
import '../../api/OAuth/server/methods';
import '../../api/Users/server/methods';
import '../../api/Users/server/publications';

View file

@ -8,7 +8,7 @@ const Authenticated = ({ loggingIn, authenticated, component, ...rest }) => (
if (loggingIn) return <div />;
return authenticated ?
(React.createElement(component, { ...props, loggingIn, authenticated })) :
(<Redirect to="/login" />);
(<Redirect to="/logout" />);
}}
/>
);

View file

@ -86,7 +86,7 @@ class DocumentEditor extends React.Component {
}
DocumentEditor.defaultProps = {
doc: PropTypes.object,
doc: { title: '', body: '' },
};
DocumentEditor.propTypes = {

View file

@ -7,7 +7,7 @@ import AuthenticatedNavigation from '../AuthenticatedNavigation/AuthenticatedNav
import './Navigation.scss';
const Navigation = ({ authenticated, name }) => (
const Navigation = props => (
<Navbar>
<Navbar.Header>
<Navbar.Brand>
@ -16,18 +16,17 @@ const Navigation = ({ authenticated, name }) => (
<Navbar.Toggle />
</Navbar.Header>
<Navbar.Collapse>
{!authenticated ? <PublicNavigation /> : <AuthenticatedNavigation name={name} />}
{!props.authenticated ? <PublicNavigation /> : <AuthenticatedNavigation {...props} />}
</Navbar.Collapse>
</Navbar>
);
Navigation.defaultProps = {
name: PropTypes.string,
name: '',
};
Navigation.propTypes = {
authenticated: PropTypes.bool.isRequired,
name: PropTypes.string,
};
export default Navigation;

View file

@ -5,58 +5,54 @@ import { Bert } from 'meteor/themeteorchef:bert';
import './OAuthLoginButton.scss';
const handleLogin = (service, options, callback) => {
const defaultOptions = {
const handleLogin = (service, callback) => {
const options = {
facebook: {
requestPermissions: ['email'],
loginStyle: 'redirect',
loginStyle: 'popup',
},
github: {
requestPermissions: ['user:email'],
loginStyle: 'popup',
},
google: {
requestPermissions: ['email'],
requestOfflineToken: true,
loginStyle: 'redirect',
},
github: {
requestPermissions: ['user:email'],
loginStyle: 'redirect',
loginStyle: 'popup',
},
}[service];
const defaultCallback = (error) => {
if (error) Bert.alert(error.reason, 'danger');
};
return {
facebook: Meteor.loginWithFacebook,
google: Meteor.loginWithGoogle,
github: Meteor.loginWithGithub,
}[service](options || defaultOptions, callback || defaultCallback);
google: Meteor.loginWithGoogle,
}[service](options, callback);
};
const serviceLabel = {
facebook: <span><i className="fa fa-facebook" /> Log In with Facebook</span>,
google: <span><i className="fa fa-google" /> Log In with Google</span>,
github: <span><i className="fa fa-github" /> Log In with GitHub</span>,
google: <span><i className="fa fa-google" /> Log In with Google</span>,
};
const OAuthLoginButton = ({ service, options, callback }) => (
const OAuthLoginButton = ({ service, callback }) => (
<button
className={`OAuthLoginButton OAuthLoginButton-${service}`}
type="button"
onClick={() => handleLogin(service, options, callback)}
onClick={() => handleLogin(service, callback)}
>
{serviceLabel[service]}
</button>
);
OAuthLoginButton.defaultProps = {
options: PropTypes.object,
callback: PropTypes.func,
callback: (error) => {
if (error) Bert.alert(error.message, 'danger');
},
};
OAuthLoginButton.propTypes = {
service: PropTypes.string.isRequired,
options: PropTypes.object,
callback: PropTypes.func,
};

View file

@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
import { ReactiveVar } from 'meteor/reactive-var';
import OAuthLoginButton from '../OAuthLoginButton/OAuthLoginButton';
import './OAuthLoginButtons.scss';
const OAuthLoginButtons = ({ services, emailMessage }) => (services.length ? (
<div className={`OAuthLoginButtons ${emailMessage ? 'WithEmailMessage' : ''}`}>
{services.map(service => <OAuthLoginButton key={service} service={service} />)}
{emailMessage ? <p className="EmailMessage" style={{ marginLeft: `-${emailMessage.offset}px` }}>
{emailMessage.text}
</p> : ''}
</div>
) : <div />);
OAuthLoginButtons.propTypes = {
services: PropTypes.array.isRequired,
emailMessage: PropTypes.object.isRequired,
};
const verificationComplete = new ReactiveVar(false);
const verifiedServices = new ReactiveVar([]);
export default createContainer(({ services }) => {
if (!verificationComplete.get()) {
Meteor.call('oauth.verifyConfiguration', services, (error, response) => {
if (error) {
console.warn(error);
} else {
verifiedServices.set(response);
verificationComplete.set(true);
}
});
}
return {
services: verifiedServices.get(),
};
}, OAuthLoginButtons);

View file

@ -1,20 +1,20 @@
@import '../../stylesheets/colors';
.Signup {
form {
position: relative;
border-top: 1px solid $gray-lighter;
margin-top: 15px;
padding-top: 25px;
.OAuthLoginButtons {
margin-bottom: 25px;
.SignupWithEmail {
&.WithEmailMessage {
position: relative;
border-bottom: 1px solid $gray-lighter;
padding-bottom: 30px;
.EmailMessage {
display: inline-block;
background: #fff;
padding: 0 10px;
position: absolute;
top: -12px;
bottom: -19px;
left: 50%;
margin-left: -106px;
}
}
}

View file

@ -15,6 +15,7 @@ import ViewDocument from '../../pages/ViewDocument/ViewDocument';
import EditDocument from '../../pages/EditDocument/EditDocument';
import Signup from '../../pages/Signup/Signup';
import Login from '../../pages/Login/Login';
import Logout from '../../pages/Logout/Logout';
import RecoverPassword from '../../pages/RecoverPassword/RecoverPassword';
import ResetPassword from '../../pages/ResetPassword/ResetPassword';
import Profile from '../../pages/Profile/Profile';
@ -34,6 +35,7 @@ const App = props => (
<Authenticated exact path="/profile" component={Profile} {...props} />
<Public path="/signup" component={Signup} {...props} />
<Public path="/login" component={Login} {...props} />
<Public path="/logout" component={Logout} {...props} />
<Route name="recover-password" path="/recover-password" component={RecoverPassword} />
<Route name="reset-password" path="/reset-password/:token" component={ResetPassword} />
<Route component={NotFound} />
@ -47,17 +49,24 @@ App.propTypes = {
loading: PropTypes.bool.isRequired,
};
const getUserName = name => ({
string: name,
object: `${name.first} ${name.last}`,
}[typeof name]);
export default createContainer(() => {
const loggingIn = Meteor.loggingIn();
const user = Meteor.user();
const userId = Meteor.userId();
const loading = !Roles.subscription.ready();
const name = user && user.profile && user.profile.name && getUserName(user.profile.name);
const emailAddress = user && user.emails && user.emails[0].address;
return {
loading,
loggingIn,
authenticated: !loggingIn && !!userId,
name: user && user.profile ? `${user.profile.name.first} ${user.profile.name.last}` : '',
name: name || emailAddress,
roles: !loading && Roles.getRolesForUser(userId),
};
}, App);

View file

@ -73,6 +73,7 @@ Documents.propTypes = {
export default createContainer(() => {
const subscription = Meteor.subscribe('documents');
console.log(DocumentsCollection.find().fetch());
return {
loading: !subscription.ready(),
documents: DocumentsCollection.find().fetch(),

View file

@ -1,17 +1,20 @@
import React from 'react';
import { Jumbotron } from 'react-bootstrap';
import { Button } from 'react-bootstrap';
import './Index.scss';
const Index = () => (
<div className="Index">
<Jumbotron>
<img
src="https://s3-us-west-2.amazonaws.com/cleverbeagle-assets/graphics/email-icon.png"
alt="Clever Beagle"
/>
<p>Need help building your app? Check out our mentorship service.</p>
</Jumbotron>
<img
src="https://s3-us-west-2.amazonaws.com/cleverbeagle-assets/graphics/email-icon.png"
alt="Clever Beagle"
/>
<h1>Pup</h1>
<p>A boilerplate for products.</p>
<div>
<Button href="https://pup.cleverbeagle.com">Read the Docs</Button>
<Button href="https://github.com/cleverbeagle/pup"><i className="fa fa-star" /> Star on GitHub</Button>
</div>
</div>
);

View file

@ -0,0 +1,58 @@
@import '../../stylesheets/mixins';
@import '../../stylesheets/colors';
.Index {
padding: 20px;
background: $cb-blue;
text-align: center;
border-radius: 3px;
color: #fff;
img {
width: 100px;
height: auto;
}
h1 {
font-size: 28px;
}
p {
font-size: 18px;
color: lighten($cb-blue, 25%);
}
> div {
display: inline-block;
margin: 10px 0 0;
.btn:first-child {
margin-right: 10px;
}
.btn {
border: none;
}
.btn:last-child {
background: darken($cb-blue, 20%);
color: lighten($cb-blue, 30%);
&:hover {
color: #fff;
}
}
}
}
@include breakpoint(tablet) {
.Index {
padding: 30px;
}
}
@include breakpoint(desktop) {
.Index {
padding: 40px;
}
}

View file

@ -4,12 +4,10 @@ import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Meteor } from 'meteor/meteor';
import { Bert } from 'meteor/themeteorchef:bert';
import OAuthLoginButton from '../../components/OAuthLoginButton/OAuthLoginButton';
import OAuthLoginButtons from '../../components/OAuthLoginButtons/OAuthLoginButtons';
import AccountPageFooter from '../../components/AccountPageFooter/AccountPageFooter';
import validate from '../../../modules/validate';
import './Login.scss';
class Login extends React.Component {
constructor(props) {
super(props);
@ -62,15 +60,16 @@ class Login extends React.Component {
<h4 className="page-header">Log In</h4>
<Row>
<Col xs={12}>
<FormGroup className="OAuthLoginButtons">
<OAuthLoginButton service="facebook" />
<OAuthLoginButton service="google" />
<OAuthLoginButton service="github" />
</FormGroup>
<OAuthLoginButtons
services={['facebook', 'github', 'google']}
emailMessage={{
offset: 100,
text: 'Log In with an Email Address',
}}
/>
</Col>
</Row>
<form ref={form => (this.form = form)} onSubmit={event => event.preventDefault()}>
<p className="LoginWithEmail">Log In with an Email Address</p>
<FormGroup>
<ControlLabel>Email Address</ControlLabel>
<input

View file

@ -1,21 +0,0 @@
@import '../../stylesheets/mixins';
@import '../../stylesheets/colors';
.Login {
form {
position: relative;
border-top: 1px solid $gray-lighter;
margin-top: 15px;
padding-top: 25px;
.LoginWithEmail {
display: inline-block;
background: #fff;
padding: 0 10px;
position: absolute;
top: -12px;
left: 50%;
margin-left: -106px;
}
}
}

View file

@ -0,0 +1,23 @@
import React from 'react';
import './Logout.scss';
const Logout = () => (
<div className="Logout">
<img
src="https://s3-us-west-2.amazonaws.com/cleverbeagle-assets/graphics/email-icon.png"
alt="Clever Beagle"
/>
<h1>Stay safe out there.</h1>
<p>{'Don\'t forget to like and follow Clever Beagle elsewhere on the web:'}</p>
<ul className="FollowUsElsewhere">
<li><a href="https://facebook.com/cleverbeagle"><i className="fa fa-facebook-official" /></a></li>
<li><a href="https://twitter.com/clvrbgl"><i className="fa fa-twitter" /></a></li>
<li><a href="https://github.com/cleverbeagle"><i className="fa fa-github" /></a></li>
</ul>
</div>
);
Logout.propTypes = {};
export default Logout;

View file

@ -0,0 +1,57 @@
@import '../../stylesheets/mixins';
@import '../../stylesheets/colors';
.Logout {
padding: 20px;
background: $cb-blue;
text-align: center;
border-radius: 3px;
color: #fff;
img {
width: 100px;
height: auto;
}
h1 {
font-size: 28px;
}
p {
font-size: 18px;
color: lighten($cb-blue, 25%);
}
ul {
list-style: none;
display: inline-block;
padding: 0;
margin: 10px 0 0;
}
ul li {
float: left;
font-size: 28px;
line-height: 28px;
a {
color: #fff;
}
&:not(:last-child) {
margin-right: 15px;
}
}
}
@include breakpoint(tablet) {
.Logout {
padding: 30px;
}
}
@include breakpoint(desktop) {
.Logout {
padding: 40px;
}
}

View file

@ -3,6 +3,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Row, Col, FormGroup, ControlLabel, Button } from 'react-bootstrap';
import { capitalize } from '@cleverbeagle/strings';
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import { Bert } from 'meteor/themeteorchef:bert';
@ -10,10 +11,17 @@ import { createContainer } from 'meteor/react-meteor-data';
import InputHint from '../../components/InputHint/InputHint';
import validate from '../../../modules/validate';
import './Profile.scss';
class Profile extends React.Component {
constructor(props) {
super(props);
this.getUserType = this.getUserType.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.renderOAuthUser = this.renderOAuthUser.bind(this);
this.renderPasswordUser = this.renderPasswordUser.bind(this);
this.renderProfileForm = this.renderProfileForm.bind(this);
}
componentDidMount() {
@ -66,6 +74,13 @@ class Profile extends React.Component {
});
}
getUserType(user) {
const userToCheck = user;
delete userToCheck.services.resume;
const service = Object.keys(userToCheck.services)[0];
return service === 'password' ? 'password' : 'oauth';
}
handleSubmit() {
const profile = {
emailAddress: this.emailAddress.value,
@ -88,6 +103,85 @@ class Profile extends React.Component {
});
}
renderOAuthUser(loading, user) {
return !loading ? (<div className="OAuthProfile">
{Object.keys(user.services).map(service => (
<div key={service} className={`LoggedInWith ${service}`}>
<div className="ServiceIcon"><i className={`fa fa-${service === 'facebook' ? 'facebook-official' : service}`} /></div>
<p>{`You're logged in with ${capitalize(service)} using the email address ${user.services[service].email}.`}</p>
</div>
))}
</div>) : <div />;
}
renderPasswordUser(loading, user) {
return !loading ? (<div>
<Row>
<Col xs={6}>
<FormGroup>
<ControlLabel>First Name</ControlLabel>
<input
type="text"
name="firstName"
defaultValue={user.profile.name.first}
ref={firstName => (this.firstName = firstName)}
className="form-control"
/>
</FormGroup>
</Col>
<Col xs={6}>
<FormGroup>
<ControlLabel>Last Name</ControlLabel>
<input
type="text"
name="lastName"
defaultValue={user.profile.name.last}
ref={lastName => (this.lastName = lastName)}
className="form-control"
/>
</FormGroup>
</Col>
</Row>
<FormGroup>
<ControlLabel>Email Address</ControlLabel>
<input
type="email"
name="emailAddress"
defaultValue={user.emails[0].address}
ref={emailAddress => (this.emailAddress = emailAddress)}
className="form-control"
/>
</FormGroup>
<FormGroup>
<ControlLabel>Current Password</ControlLabel>
<input
type="password"
name="currentPassword"
ref={currentPassword => (this.currentPassword = currentPassword)}
className="form-control"
/>
</FormGroup>
<FormGroup>
<ControlLabel>New Password</ControlLabel>
<input
type="password"
name="newPassword"
ref={newPassword => (this.newPassword = newPassword)}
className="form-control"
/>
<InputHint>Use at least six characters.</InputHint>
</FormGroup>
<Button type="submit" bsStyle="success">Save Profile</Button>
</div>) : <div />;
}
renderProfileForm(loading, user) {
return !loading ? ({
password: this.renderPasswordUser,
oauth: this.renderOAuthUser,
}[this.getUserType(user)])(loading, user) : <div />;
}
render() {
const { loading, user } = this.props;
return (<div className="Profile">
@ -95,65 +189,7 @@ class Profile extends React.Component {
<Col xs={12} sm={6} md={4}>
<h4 className="page-header">Edit Profile</h4>
<form ref={form => (this.form = form)} onSubmit={event => event.preventDefault()}>
{!loading ? (<div>
<Row>
<Col xs={6}>
<FormGroup>
<ControlLabel>First Name</ControlLabel>
<input
type="text"
name="firstName"
defaultValue={user.profile.name.first}
ref={firstName => (this.firstName = firstName)}
className="form-control"
/>
</FormGroup>
</Col>
<Col xs={6}>
<FormGroup>
<ControlLabel>Last Name</ControlLabel>
<input
type="text"
name="lastName"
defaultValue={user.profile.name.last}
ref={lastName => (this.lastName = lastName)}
className="form-control"
/>
</FormGroup>
</Col>
</Row>
<FormGroup>
<ControlLabel>Email Address</ControlLabel>
<input
type="email"
name="emailAddress"
defaultValue={user.emails[0].address}
ref={emailAddress => (this.emailAddress = emailAddress)}
className="form-control"
/>
</FormGroup>
<h5 className="page-header">Change Password</h5>
<FormGroup>
<ControlLabel>Current Password</ControlLabel>
<input
type="password"
name="currentPassword"
ref={currentPassword => (this.currentPassword = currentPassword)}
className="form-control"
/>
</FormGroup>
<FormGroup>
<ControlLabel>New Password</ControlLabel>
<input
type="password"
name="newPassword"
ref={newPassword => (this.newPassword = newPassword)}
className="form-control"
/>
<InputHint>Use at least six characters.</InputHint>
</FormGroup>
<Button type="submit" bsStyle="success">Save Profile</Button>
</div>) : <div />}
{this.renderProfileForm(loading, user)}
</form>
</Col>
</Row>

View file

@ -0,0 +1,39 @@
@import '../../stylesheets/colors';
.OAuthProfile {
.LoggedInWith {
padding: 10px 20px;
border-radius: 3px;
color: #fff;
p {
margin: 0;
}
.ServiceIcon {
float: left;
width: auto;
margin-right: 15px;
height: 30px;
text-align: center;
i {
font-size: 30px;
position: relative;
top: 5px;
}
}
&.facebook {
background: $facebook;
}
&.github {
background: $github;
}
&.google {
background: $google;
}
}
}

View file

@ -4,13 +4,11 @@ import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Accounts } from 'meteor/accounts-base';
import { Bert } from 'meteor/themeteorchef:bert';
import OAuthLoginButton from '../../components/OAuthLoginButton/OAuthLoginButton';
import OAuthLoginButtons from '../../components/OAuthLoginButtons/OAuthLoginButtons';
import InputHint from '../../components/InputHint/InputHint';
import AccountPageFooter from '../../components/AccountPageFooter/AccountPageFooter';
import validate from '../../../modules/validate';
import './Signup.scss';
class Signup extends React.Component {
constructor(props) {
super(props);
@ -86,15 +84,16 @@ class Signup extends React.Component {
<h4 className="page-header">Sign Up</h4>
<Row>
<Col xs={12}>
<FormGroup className="OAuthLoginButtons">
<OAuthLoginButton service="facebook" />
<OAuthLoginButton service="google" />
<OAuthLoginButton service="github" />
</FormGroup>
<OAuthLoginButtons
services={['facebook', 'github', 'google']}
emailMessage={{
offset: 97,
text: 'Sign Up with an Email Address',
}}
/>
</Col>
</Row>
<form ref={form => (this.form = form)} onSubmit={event => event.preventDefault()}>
<p className="SignupWithEmail">Sign Up with an Email Address</p>
<Row>
<Col xs={6}>
<FormGroup>

View file

@ -13,3 +13,8 @@ $gray-lighter: #eee;
$facebook: #3b5998;
$google: #ea4335;
$github: $gray-dark;
$cb-blue: #4285F4;
$cb-green: #00D490;
$cb-yellow: #FFCF50;
$cb-red: #DA5847;