mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2025-03-14 15:09:59 -04:00
Merge pull request #15 from rschamp/feature/session-api
Share session from scratchr2, add logging in/out
This commit is contained in:
commit
6fba6c0a8f
14 changed files with 193 additions and 54 deletions
10
README.md
10
README.md
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
12
server/proxies.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
[
|
||||
{
|
||||
"root": "/",
|
||||
"paths": [
|
||||
"/accounts/",
|
||||
"/accounts/",
|
||||
"/csrf_token/",
|
||||
"/get_image/",
|
||||
"/session/"
|
||||
]
|
||||
}
|
||||
]
|
|
@ -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} />;
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
3
src/log.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
var bunyan = require('bunyan');
|
||||
|
||||
module.exports = bunyan.createLogger({name: 'scratch-www'});
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -48,6 +48,9 @@ module.exports = {
|
|||
}
|
||||
]
|
||||
},
|
||||
node: {
|
||||
fs: 'empty'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': Object.keys(buildEnv).reduce(function (env, key) {
|
||||
|
|
Loading…
Reference in a new issue