Merge pull request #15 from rschamp/feature/session-api

Share session from scratchr2, add logging in/out
This commit is contained in:
Ray Schamp 2015-10-07 15:27:29 -04:00
commit 6fba6c0a8f
14 changed files with 193 additions and 54 deletions

View file

@ -18,6 +18,16 @@ npm start
Once running, open `http://localhost:8333` in your browser. If you wish to have the server reload automatically, you can install either [nodemon](https://github.com/remy/nodemon) or [forever](https://github.com/foreverjs/forever).
#### Configuration
`npm start` and `npm run watch` can be configured with the following environment variables
| Variable | Default | Description |
| ------------- | --------------------------------- | ---------------------------------------------- |
| `NODE_ENV` | `null` | If not `production`, app acts like development |
| `PORT` | `8333` | Port for devserver (http://localhost:XXXX) |
| `PROXY_HOST` | `https://staging.scratch.mit.edu` | Pass-through location for scratchr2 |
### To Test
```bash
npm test

View file

@ -21,15 +21,17 @@
},
"homepage": "https://github.com/llk/scratch-www#readme",
"dependencies": {
"bunyan": "1.4.0",
"bunyan": "1.5.0",
"compression": "1.5.2",
"express": "4.13.3",
"express-http-proxy": "0.6.0",
"lodash.defaults": "3.1.2",
"mustache": "2.1.3"
},
"devDependencies": {
"autoprefixer-loader": "2.1.0",
"classnames": "2.1.3",
"cookie": "0.2.2",
"css-loader": "0.17.0",
"custom-event-polyfill": "0.2.1",
"eslint": "1.3.1",
@ -46,6 +48,7 @@
"sass-lint": "1.2.0",
"sass-loader": "2.0.1",
"slick-carousel": "1.5.8",
"source-map-support": "0.3.2",
"style-loader": "0.12.3",
"tape": "4.2.0",
"url-loader": "0.5.6",

View file

@ -1,5 +1,6 @@
var compression = require('compression');
var express = require('express');
var proxy = require('express-http-proxy');
var _path = require('path');
var handler = require('./handler');
@ -13,8 +14,8 @@ app.use(log());
app.use(compression());
// Bind routes
for (var item in routes) {
var route = routes[item];
for (var routeId in routes) {
var route = routes[routeId];
if ( route.static ) {
app.use( express.static( eval( route.resolve ), route.attributes ) );
} else {
@ -22,8 +23,33 @@ for (var item in routes) {
}
}
// Bind proxies in development
if ( process.env.NODE_ENV != 'production' ) {
var proxies = require('./proxies.json');
var url = require('url');
var proxyHost = process.env.PROXY_HOST || 'https://staging.scratch.mit.edu';
for (var proxyId in proxies) {
var proxyRoute = proxies[proxyId];
app.use(proxyRoute.root, proxy(proxyRoute.proxy || proxyHost, {
filter: function (req) {
for (var pathId in proxyRoute.paths) {
var path = proxyRoute.paths[pathId];
if (url.parse(req.url).path.indexOf(path) == 0) return true;
}
return false;
},
forwardPath: function (req) {
return url.parse(req.url).path;
}
}));
}
}
// Start listening
var port = process.env.PORT || 8333;
app.listen(port, function () {
process.stdout.write('Server listening on port ' + port + '\n');
if (proxyHost) {
process.stdout.write('Proxy host: ' + proxyHost + '\n');
}
});

12
server/proxies.json Normal file
View file

@ -0,0 +1,12 @@
[
{
"root": "/",
"paths": [
"/accounts/",
"/accounts/",
"/csrf_token/",
"/get_image/",
"/session/"
]
}
]

View file

@ -1,31 +1,20 @@
var React = require('react');
var classNames = require('classnames');
module.exports = React.createClass({
propTypes: {
path: React.PropTypes.string,
userId: React.PropTypes.number,
size: React.PropTypes.number,
extension: React.PropTypes.string,
version: React.PropTypes.number
src: React.PropTypes.string
},
getDefaultProps: function () {
return {
path: '//cdn2.scratch.mit.edu/get_image/user/',
userId: 2584924,
size: 32,
extension: 'png',
version: 1438702210.96
src: '//cdn2.scratch.mit.edu/get_image/user/2584924_24x24.png?v=1438702210.96'
};
},
getImageUrl: function () {
return (
this.props.path + this.props.userId + '_' +
this.props.size + 'x' + this.props.size + '.' +
this.props.extension + '?v=' + this.props.version);
},
render: function () {
var url = this.getImageUrl();
return (
<img className="avatar" src={url} />);
var classes = classNames(
'avatar',
this.props.className
);
return <img {... this.props} className={classes} />;
}
});

View file

@ -6,22 +6,31 @@ require('./login.scss');
module.exports = React.createClass({
propTypes: {
onLogIn: React.PropTypes.func
onLogIn: React.PropTypes.func,
error: React.PropTypes.string
},
handleSubmit: function (event) {
event.preventDefault();
this.props.onLogIn();
this.props.onLogIn({
'username': this.refs.username.getDOMNode().value,
'password': this.refs.password.getDOMNode().value
});
},
render: function () {
var error;
if (this.props.error) {
error = <div className="error">{this.props.error}</div>;
}
return (
<div className="login">
<form onSubmit={this.handleSubmit}>
<label htmlFor="username">Username</label>
<Input type="text" name="username" maxLength="30" />
<Input type="text" ref="username" name="username" maxLength="30" />
<label htmlFor="password">Password</label>
<Input type="password" name="password" />
<Input type="password" ref="password" name="password" />
<Button className="submit-button white" type="submit">Sign in</Button>
<a className="right" href="/accounts/password_reset/">Forgot password?</a>
{error}
</form>
</div>
);

View file

@ -1,3 +1,5 @@
@import "../../colors";
.login {
padding: 10px;
@ -17,4 +19,11 @@
a:hover {
background-color: transparent;
}
.error {
border: 1px solid $active-dark-gray;
border-radius: 5px;
background-color: $ui-orange;
padding: .75em 1em;
}
}

View file

@ -1,21 +1,28 @@
var React = require('react');
var classNames = require('classnames');
var xhr = require('xhr');
var log = require('../../log.js');
var Api = require('../../mixins/api.jsx');
var Session = require('../../mixins/session.jsx');
var Avatar = require('../avatar/avatar.jsx');
var Dropdown = require('./dropdown.jsx');
var Input = require('../forms/input.jsx');
var Login = require('../login/login.jsx');
var Session = require('../../mixins/session.jsx');
require('./navigation.scss');
module.exports = React.createClass({
mixins: [
Api,
Session
],
getInitialState: function () {
return {
'loginOpen': false,
'loginError': null,
'accountNavOpen': false
};
},
@ -26,13 +33,36 @@ module.exports = React.createClass({
closeLogin: function () {
this.setState({'loginOpen': false});
},
handleLogIn: function () {
// @todo Use an api
window.updateSession(require('../../session.json'));
handleLogIn: function (formData) {
this.setState({'loginError': null});
this.api({
method: 'post',
uri: '/accounts/login/',
json: formData,
useCsrf: true
}, function (err, body) {
if (body) {
body = body[0];
if (!body.success) {
this.setState({'loginError': body.msg});
} else {
this.closeLogin();
window.refreshSession();
}
}
}.bind(this));
},
handleLogOut: function () {
// @todo Use an api
window.updateSession({});
xhr({
uri: '/accounts/logout/'
}, function (err) {
if (err) {
log.error(err);
} else {
this.closeLogin();
window.refreshSession();
}
}.bind(this));
},
handleClickAccountNav: function () {
this.setState({'accountNavOpen': true});
@ -41,10 +71,9 @@ module.exports = React.createClass({
this.setState({'accountNavOpen': false});
},
render: function () {
var loggedIn = !!this.state.session.token;
var classes = classNames({
'inner': true,
'logged-in': this.state.loggedIn
'logged-in': this.state.session.user
});
return (
<div className={classes}>
@ -65,7 +94,7 @@ module.exports = React.createClass({
<Input type="hidden" name="sort_by" value="datetime_shared" />
</form>
</li>
{loggedIn ? [
{this.state.session.user ? [
<li className="link right messages" key="messages">
<a href="/messages/" title="Messages">Messages</a>
</li>,
@ -74,10 +103,7 @@ module.exports = React.createClass({
</li>,
<li className="link right account-nav" key="account-nav">
<a className="userInfo" href="#" onClick={this.handleClickAccountNav}>
<Avatar
userId={this.state.session.user.id}
version={this.state.session.user.avatarVersion}
size={24} />
<Avatar src={this.state.session.user.thumbnailUrl} />
{this.state.session.user.username}
</a>
<Dropdown
@ -100,7 +126,9 @@ module.exports = React.createClass({
className="login-dropdown with-arrow"
isOpen={this.state.loginOpen}
onRequestClose={this.closeLogin}>
<Login onLogIn={this.handleLogIn} />
<Login
onLogIn={this.handleLogIn}
error={this.state.loginError} />
</Dropdown>
</li>
]}

3
src/log.js Normal file
View file

@ -0,0 +1,3 @@
var bunyan = require('bunyan');
module.exports = bunyan.createLogger({name: 'scratch-www'});

View file

@ -1,14 +1,55 @@
var cookie = require('cookie');
var defaults = require('lodash.defaults');
var xhr = require('xhr');
var log = require('../log.js');
module.exports = {
getCsrf: function (callback) {
var obj = cookie.parse(document.cookie) || {};
if (typeof obj.scratchcsrftoken === 'undefined') return callback('Cookie not found.');
callback(null, obj.scratchcsrftoken);
},
useCsrf: function (callback) {
this.getCsrf(function (err, csrftoken) {
if (csrftoken) return callback(null, csrftoken);
xhr({
'uri': '/csrf_token/'
}, function (err) {
if (err) return callback(err);
this.getCsrf(function (err, csrftoken) {
if (err) return callback(err);
callback(err, csrftoken);
});
}.bind(this));
}.bind(this));
},
api: function (opts, callback) {
xhr(opts, function (err, res, body) {
if (err) {
// emit global "error" event
return callback(err);
}
// @todo Global error handler
callback(err, body);
defaults(opts, {
headers: {},
json: {},
useCsrf: false
});
defaults(opts.headers, {
'X-Requested-With': 'XMLHttpRequest'
});
var apiRequest = function (opts) {
xhr(opts, function (err, res, body) {
if (err) log.error(err);
callback(err, body);
});
}.bind(this);
if (opts.useCsrf) {
this.useCsrf(function (err, csrftoken) {
if (err) return log.error('Error while retrieving CSRF token', err);
opts.json.csrftoken = csrftoken;
opts.headers['X-CSRFToken'] = csrftoken;
apiRequest(opts);
}.bind(this));
} else {
apiRequest(opts);
}
}
};

View file

@ -1,4 +1,4 @@
require('xhr');
var api = require('./mixins/api.jsx').api;
require('custom-event-polyfill');
window._session = {};
@ -9,5 +9,12 @@ window.updateSession = function (session) {
window.dispatchEvent(sessionEvent);
};
// @todo Get the session from an API
window.updateSession({});
window.refreshSession = function () {
api({
uri: '/session/'
}, function (err, body) {
window.updateSession(body);
});
};
window.refreshSession();

View file

@ -3,7 +3,7 @@
"user": {
"id": 1709047,
"username": "thisandagain",
"avatarVersion": 1438702210.96
"thumbnailUrl": "//cdn2.scratch.mit.edu/get_image/user/1709047_32x32.png?v=1427980331.0"
},
"permissions": {
"admin": true,

View file

@ -30,11 +30,10 @@ var View = React.createClass({
// @todo API request for Featured
},
render: function () {
var loggedIn = !!this.state.session.token;
return (
<div className="inner">
{loggedIn ? [
<div className="splash-header">
{this.state.session.user ? [
<div key="header" className="splash-header">
<Activity />
<News />
</div>

View file

@ -48,6 +48,9 @@ module.exports = {
}
]
},
node: {
fs: 'empty'
},
plugins: [
new webpack.DefinePlugin({
'process.env': Object.keys(buildEnv).reduce(function (env, key) {