Dump of changes
parent
d8b0554682
commit
35408b43e2
|
@ -9,4 +9,4 @@ dist/
|
||||||
# Temporary files
|
# Temporary files
|
||||||
oid_settings.json
|
oid_settings.json
|
||||||
nginx.conf
|
nginx.conf
|
||||||
sample_data.sql
|
sample_*.sql
|
||||||
|
|
95
Gruntfile.js
95
Gruntfile.js
|
@ -7,7 +7,7 @@ module.exports = function (grunt) {
|
||||||
entry: __dirname + '/app/index.jsx',
|
entry: __dirname + '/app/index.jsx',
|
||||||
output: {
|
output: {
|
||||||
filename: 'bundle.js',
|
filename: 'bundle.js',
|
||||||
path: __dirname + '/dist/app/'
|
path: __dirname + '/dist/app/',
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.jsx'],
|
extensions: ['.js', '.jsx'],
|
||||||
|
@ -25,15 +25,15 @@ module.exports = function (grunt) {
|
||||||
presets: [
|
presets: [
|
||||||
['env', {
|
['env', {
|
||||||
targets: {
|
targets: {
|
||||||
browsers: ['last 2 versions']
|
browsers: ['last 2 versions'],
|
||||||
},
|
},
|
||||||
modules: false
|
modules: false,
|
||||||
}],
|
}],
|
||||||
'react'
|
'react',
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
|
@ -45,17 +45,17 @@ module.exports = function (grunt) {
|
||||||
sourceMap: true,
|
sourceMap: true,
|
||||||
modules: true,
|
modules: true,
|
||||||
importLoaders: 1,
|
importLoaders: 1,
|
||||||
localIdentName: '[name]--[local]--[hash:base64:8]'
|
localIdentName: '[name]--[local]--[hash:base64:8]',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loader: 'postcss-loader'
|
loader: 'postcss-loader',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
copy: {
|
copy: {
|
||||||
app: {
|
app: {
|
||||||
|
@ -64,34 +64,37 @@ module.exports = function (grunt) {
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: __dirname + '/app/',
|
cwd: __dirname + '/app/',
|
||||||
src: ['index.html'],
|
src: ['index.html'],
|
||||||
dest: __dirname + '/dist/app/'
|
dest: __dirname + '/dist/app/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: __dirname + '/app/',
|
cwd: __dirname + '/app/',
|
||||||
src: ['assets/**'],
|
src: ['assets/**'],
|
||||||
dest: __dirname + '/dist/app/assets/'
|
dest: __dirname + '/dist/app/assets/',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: __dirname + '/node_modules/oidc-client/',
|
cwd: __dirname + '/node_modules/oidc-client/',
|
||||||
src: ['dist/**'],
|
src: ['dist/**'],
|
||||||
dest: __dirname + '/dist/app/assets/oidc-client/'
|
dest: __dirname + '/dist/app/assets/oidc-client/',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
babel: {
|
babel: {
|
||||||
options: {
|
options: {
|
||||||
sourceMap: true,
|
sourceMap: true,
|
||||||
presets: [
|
presets: [
|
||||||
['env', {
|
[
|
||||||
targets: {
|
'env',
|
||||||
node: 'current'
|
{
|
||||||
|
targets: {
|
||||||
|
node: 'current',
|
||||||
|
},
|
||||||
|
modules: 'commonjs',
|
||||||
},
|
},
|
||||||
modules: 'commonjs'
|
],
|
||||||
}]
|
],
|
||||||
]
|
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
files: [
|
files: [
|
||||||
|
@ -99,10 +102,10 @@ module.exports = function (grunt) {
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: __dirname + '/server/',
|
cwd: __dirname + '/server/',
|
||||||
src: ['**/*.js'],
|
src: ['**/*.js'],
|
||||||
dest: __dirname + '/dist/server/'
|
dest: __dirname + '/dist/server/',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
eslint: {
|
eslint: {
|
||||||
app: {
|
app: {
|
||||||
|
@ -111,9 +114,9 @@ module.exports = function (grunt) {
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: __dirname + '/app/',
|
cwd: __dirname + '/app/',
|
||||||
src: ['**/*.js*'],
|
src: ['**/*.js*'],
|
||||||
dest: __dirname + '/dist/app/'
|
dest: __dirname + '/dist/app/',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
files: [
|
files: [
|
||||||
|
@ -121,27 +124,27 @@ module.exports = function (grunt) {
|
||||||
expand: true,
|
expand: true,
|
||||||
cwd: __dirname + '/server/',
|
cwd: __dirname + '/server/',
|
||||||
src: ['**/*.js'],
|
src: ['**/*.js'],
|
||||||
dest: __dirname + '/dist/server/'
|
dest: __dirname + '/dist/server/',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
app: {
|
app: {
|
||||||
files: '**/*.js*',
|
files: '**/*.js*',
|
||||||
tasks: ['app'],
|
tasks: ['app'],
|
||||||
options: {
|
options: {
|
||||||
cwd: __dirname + '/app/'
|
cwd: __dirname + '/app/',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
files: '**/*.js',
|
files: '**/*.js',
|
||||||
tasks: ['server'],
|
tasks: ['server'],
|
||||||
options: {
|
options: {
|
||||||
cwd: __dirname + '/server/'
|
cwd: __dirname + '/server/',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
grunt.loadNpmTasks('grunt-webpack');
|
grunt.loadNpmTasks('grunt-webpack');
|
||||||
|
|
|
@ -31,4 +31,5 @@ A school event planner and timetable
|
||||||
## Security Pitfalls
|
## Security Pitfalls
|
||||||
|
|
||||||
- Auth mechanism not verified
|
- Auth mechanism not verified
|
||||||
|
- Verification of OID tokens is done by upn being the email address
|
||||||
- Succeptable to insecure direct object references
|
- Succeptable to insecure direct object references
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Dialog, Input, Dropdown, DatePicker, TimePicker } from 'react-toolbox';
|
||||||
|
|
||||||
|
export default class AddEventDialog extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const now = new Date();
|
||||||
|
this.state = {
|
||||||
|
group: null,
|
||||||
|
name: '',
|
||||||
|
start: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 8),
|
||||||
|
end: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 10),
|
||||||
|
groups: [
|
||||||
|
{ value: 1, label: 'M17502' },
|
||||||
|
{ value: 2, label: 'Infocomm Club' },
|
||||||
|
{ value: 3, label: 'Youth Flying Club', disabled: true },
|
||||||
|
{ value: 4, label: 'Engineering Intrest Group' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
this.handleGroupChange = this.handleGroupChange.bind(this);
|
||||||
|
this.handleNameChange = this.handleNameChange.bind(this);
|
||||||
|
this.handleStartChange = this.handleStartChange.bind(this);
|
||||||
|
this.handleEndChange = this.handleEndChange.bind(this);
|
||||||
|
|
||||||
|
this.actions = [
|
||||||
|
{ label: 'Cancel', onClick: this.props.onCancel, accent: true },
|
||||||
|
{ label: 'Add', onClick: this.addEvent, accent: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
// pull user groups, together with the relationship attribute role
|
||||||
|
// put user groups into state.groups, disabling those not admin
|
||||||
|
}
|
||||||
|
|
||||||
|
handleGroupChange(value) {
|
||||||
|
this.setState({
|
||||||
|
group: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNameChange(value) {
|
||||||
|
this.setState({
|
||||||
|
name: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStartChange(value) {
|
||||||
|
this.setState(prev => ({
|
||||||
|
start: value,
|
||||||
|
end: new Date(
|
||||||
|
value.getFullYear(),
|
||||||
|
value.getMonth(),
|
||||||
|
value.getDate(),
|
||||||
|
prev.end.getHours(),
|
||||||
|
prev.end.getMinutes(),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
handleEndChange(value) {
|
||||||
|
this.setState({
|
||||||
|
end: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
{...this.props}
|
||||||
|
actions={this.actions}
|
||||||
|
title="Create a one-time event"
|
||||||
|
>
|
||||||
|
<Dropdown
|
||||||
|
auto
|
||||||
|
label="Group"
|
||||||
|
source={this.state.groups}
|
||||||
|
value={this.state.group}
|
||||||
|
required
|
||||||
|
onChange={this.handleGroupChange}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
label="Name"
|
||||||
|
value={this.state.name}
|
||||||
|
required
|
||||||
|
onChange={this.handleNameChange}
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
label="Start Date"
|
||||||
|
minDate={new Date()}
|
||||||
|
value={this.state.start}
|
||||||
|
required
|
||||||
|
onChange={this.handleStartChange}
|
||||||
|
/>
|
||||||
|
<TimePicker
|
||||||
|
label="Start Time"
|
||||||
|
value={this.state.start}
|
||||||
|
required
|
||||||
|
onChange={this.handleStartChange}
|
||||||
|
/>
|
||||||
|
<DatePicker
|
||||||
|
label="End Date"
|
||||||
|
minDate={
|
||||||
|
new Date(
|
||||||
|
this.state.start.getFullYear(),
|
||||||
|
this.state.start.getMonth(),
|
||||||
|
this.state.start.getDate(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={this.state.end}
|
||||||
|
required
|
||||||
|
onChange={this.handleEndChange}
|
||||||
|
/>
|
||||||
|
<TimePicker
|
||||||
|
label="End Time"
|
||||||
|
value={this.state.end}
|
||||||
|
required
|
||||||
|
onChange={this.handleEndChange}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddEventDialog.propTypes = {
|
||||||
|
onCancel: React.PropTypes.func.isRequired,
|
||||||
|
};
|
|
@ -0,0 +1,34 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouter as Router, Route } from 'react-router-dom';
|
||||||
|
|
||||||
|
import LayoutMain from '../layouts/main';
|
||||||
|
import PageHome from '../pages/home';
|
||||||
|
import PageLogin from '../pages/login';
|
||||||
|
import PageLoginSchool from '../pages/login_school';
|
||||||
|
|
||||||
|
export default class App extends React.Component {
|
||||||
|
getChildContext() {
|
||||||
|
return {
|
||||||
|
user: {},
|
||||||
|
token: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<LayoutMain>
|
||||||
|
<Route exact path="/" component={PageHome} />
|
||||||
|
<Route path="/login" component={PageLogin} />
|
||||||
|
<Route path="/login/:id" component={PageLoginSchool} />
|
||||||
|
</LayoutMain>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
App.childContextTypes = {
|
||||||
|
// eslint-disable-next-line react/forbid-prop-types
|
||||||
|
user: React.PropTypes.object.isRequired,
|
||||||
|
token: React.PropTypes.string,
|
||||||
|
};
|
|
@ -1,6 +1,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>Chronos</title>
|
<title>Chronos</title>
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
<link href="//fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
<link href="//fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||||
<link href="//fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet" />
|
<link href="//fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet" />
|
||||||
<style>*, *:before, *:after { box-sizing: border-box; } html, body { margin: 0; padding: 0; }</style>
|
<style>*, *:before, *:after { box-sizing: border-box; } html, body { margin: 0; padding: 0; }</style>
|
||||||
|
|
|
@ -2,23 +2,10 @@ import 'babel-polyfill';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { BrowserRouter as Router, Route } from 'react-router-dom';
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
import App from './components/app';
|
||||||
import * as oidc from 'oidc-client';
|
|
||||||
|
|
||||||
import LayoutMain from './layouts/main';
|
|
||||||
import PageHome from './pages/home';
|
|
||||||
import PageLogin from './pages/login';
|
|
||||||
import PageLoginSchool from './pages/login_school';
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Router>
|
<App />,
|
||||||
<LayoutMain>
|
|
||||||
<Route exact path="/" component={PageHome} />
|
|
||||||
<Route path="/login" component={PageLogin} />
|
|
||||||
<Route path="/login/:id" component={PageLoginSchool} />
|
|
||||||
</LayoutMain>
|
|
||||||
</Router>,
|
|
||||||
document.getElementById('root'),
|
document.getElementById('root'),
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default class LayoutMain extends React.Component {
|
||||||
|
|
||||||
toggleDrawerActive() {
|
toggleDrawerActive() {
|
||||||
this.setState({
|
this.setState({
|
||||||
drawerActive: !this.state.drawerActive,
|
drawerActive: !this.state.drawerActive, // TODO: use function instead
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,11 +27,14 @@ export default class LayoutMain extends React.Component {
|
||||||
onOverlayClick={this.toggleDrawerActive}
|
onOverlayClick={this.toggleDrawerActive}
|
||||||
>
|
>
|
||||||
<div style={{ fontSize: '1.2em' }}>
|
<div style={{ fontSize: '1.2em' }}>
|
||||||
{this.props.user ? `Hello, ${this.props.user.name}!` : 'Not logged in'}
|
{this.context.user.email ?
|
||||||
|
<span>
|
||||||
|
Hello, <span title={this.context.user.email}>{this.context.user.name}</span>!
|
||||||
|
</span> : 'Not logged in'}
|
||||||
</div>
|
</div>
|
||||||
<List selectable ripple>
|
<List selectable ripple>
|
||||||
{this.props.user ? <ListItem caption="Home" onClick={() => this.context.router.history.push('/')} /> : null}
|
{this.context.user.email ? <ListItem caption="Home" onClick={() => this.context.router.history.push('/')} /> : null}
|
||||||
{this.props.user ? <ListItem caption="Logout" /> : null}
|
{this.context.user.email ? <ListItem caption="Logout" /> : null}
|
||||||
<ListItem caption="Help" />
|
<ListItem caption="Help" />
|
||||||
<ListItem caption="About" />
|
<ListItem caption="About" />
|
||||||
<ListItem to="https://github.com/ambrosechua/chronos" caption="GitHub" />
|
<ListItem to="https://github.com/ambrosechua/chronos" caption="GitHub" />
|
||||||
|
@ -40,8 +43,7 @@ export default class LayoutMain extends React.Component {
|
||||||
<Panel>
|
<Panel>
|
||||||
<AppBar title="Chronos" leftIcon="menu" onLeftIconClick={this.toggleDrawerActive}>
|
<AppBar title="Chronos" leftIcon="menu" onLeftIconClick={this.toggleDrawerActive}>
|
||||||
<Navigation type="horizontal">
|
<Navigation type="horizontal">
|
||||||
<Link label="Inbox" icon="inbox" onClick={() => this.context.router.history.push('/login')} />
|
<Link label="Login" onClick={() => this.context.router.history.push('/login')} style={{ color: 'var(--color-dark-contrast)' }} />
|
||||||
<Link active label="Profile" icon="person" />
|
|
||||||
</Navigation>
|
</Navigation>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
|
@ -53,14 +55,10 @@ export default class LayoutMain extends React.Component {
|
||||||
|
|
||||||
LayoutMain.defaultProps = {
|
LayoutMain.defaultProps = {
|
||||||
children: null,
|
children: null,
|
||||||
user: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
LayoutMain.propTypes = {
|
LayoutMain.propTypes = {
|
||||||
children: React.PropTypes.node,
|
children: React.PropTypes.node,
|
||||||
user: React.PropTypes.shape({
|
|
||||||
name: React.PropTypes.string,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
LayoutMain.contextTypes = {
|
LayoutMain.contextTypes = {
|
||||||
|
@ -69,5 +67,7 @@ LayoutMain.contextTypes = {
|
||||||
push: React.PropTypes.func.isRequired,
|
push: React.PropTypes.func.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
// eslint-disable-next-line react/forbid-prop-types
|
||||||
|
user: React.PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Card, CardTitle, CardText, CardActions, Button } from 'react-toolbox';
|
import { Card, CardTitle, CardText, CardActions, Button } from 'react-toolbox';
|
||||||
|
// import BigCalendar from 'react-big-calendar';
|
||||||
|
// import moment from 'moment';
|
||||||
|
// BigCalendar.setLocalizer(BigCalendar.momentLocalizer(moment));
|
||||||
|
|
||||||
|
import AddEventDialog from '../components/addeventdialog';
|
||||||
|
|
||||||
// eslint-disable-next-line react/prefer-stateless-function
|
// eslint-disable-next-line react/prefer-stateless-function
|
||||||
export default class PageHome extends React.Component {
|
export default class PageHome extends React.Component {
|
||||||
|
@ -10,22 +15,47 @@ export default class PageHome extends React.Component {
|
||||||
cards: [
|
cards: [
|
||||||
{ key: 0, title: 'Math Test', description: 'Meow' },
|
{ key: 0, title: 'Math Test', description: 'Meow' },
|
||||||
],
|
],
|
||||||
|
addEventDialogActive: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.showAddEventDialog = this.showAddEventDialog.bind(this);
|
||||||
|
this.hideAddEventDialog = this.hideAddEventDialog.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
showAddEventDialog() {
|
||||||
|
this.setState({
|
||||||
|
addEventDialogActive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAddEventDialog() {
|
||||||
|
this.setState({
|
||||||
|
addEventDialogActive: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '1em' }}>
|
<main>
|
||||||
{this.state.cards.map(card =>
|
<div style={{ padding: '1em' }}>
|
||||||
<Card key={card.key} style={{ margin: '1em', width: 'auto' }}>
|
{this.state.cards.map(card =>
|
||||||
<CardTitle title={card.title} />
|
<Card key={card.key} style={{ margin: '1em', width: 'auto' }}>
|
||||||
<CardText>{card.description}</CardText>
|
<CardTitle title={card.title} />
|
||||||
<CardActions>
|
<CardText>{card.description}</CardText>
|
||||||
<Button label="Edit" />
|
<CardActions>
|
||||||
</CardActions>
|
<Button label="Edit" accent />
|
||||||
</Card>,
|
</CardActions>
|
||||||
)}
|
</Card>,
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
<Button style={{ position: 'fixed', bottom: '1em', right: '1em' }} icon="add" floating accent onClick={this.showAddEventDialog} />
|
||||||
|
<AddEventDialog
|
||||||
|
active={this.state.addEventDialogActive}
|
||||||
|
onCancel={this.hideAddEventDialog}
|
||||||
|
onEscKeyDown={this.hideAddEventDialog}
|
||||||
|
onOverlayClick={this.hideAddEventDialog}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,21 @@ import { Input, Button, Snackbar } from 'react-toolbox';
|
||||||
|
|
||||||
import { UserManager } from 'oidc-client';
|
import { UserManager } from 'oidc-client';
|
||||||
|
|
||||||
|
const getParams = (query) => {
|
||||||
|
if (!query) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (/^[?#]/.test(query) ? query.slice(1) : query)
|
||||||
|
.split('&')
|
||||||
|
.reduce((params, param) => {
|
||||||
|
const [key, value] = param.split('=');
|
||||||
|
const obj = {};
|
||||||
|
obj[key] = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : '';
|
||||||
|
return Object.assign(params, obj);
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line react/prefer-stateless-function
|
// eslint-disable-next-line react/prefer-stateless-function
|
||||||
export default class PageLoginSchool extends React.Component {
|
export default class PageLoginSchool extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -26,16 +41,33 @@ export default class PageLoginSchool extends React.Component {
|
||||||
client_id: this.state.school.auth[0].oid_cid,
|
client_id: this.state.school.auth[0].oid_cid,
|
||||||
redirect_uri: `${window.location.origin}/login/${this.state.school.id}`,
|
redirect_uri: `${window.location.origin}/login/${this.state.school.id}`,
|
||||||
});
|
});
|
||||||
this.userManager.getUser()
|
const params = getParams(window.location.hash);
|
||||||
.then((user) => {
|
if (params.id_token) {
|
||||||
console.log(user);
|
// TODO: check auth by sending request to server
|
||||||
if (user) {
|
// TODO: auth endpoint should return user information
|
||||||
this.context.router.history.push('/');
|
// TODO: use user information here:
|
||||||
}
|
const method = 'POST';
|
||||||
})
|
const headers = new Headers();
|
||||||
.catch(() => {
|
headers.append('Content-Type', 'application/json');
|
||||||
|
const body = JSON.stringify({
|
||||||
});
|
type: 'OID',
|
||||||
|
id_token: params.id_token,
|
||||||
|
});
|
||||||
|
fetch(`/api/v1/schools/${this.state.school.id}/login`, {
|
||||||
|
method, headers, body,
|
||||||
|
}).then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
Object.assign(this.context.user, await response.json());
|
||||||
|
this.context.router.history.push('/');
|
||||||
|
} else {
|
||||||
|
console.error(await response.json());
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
}).catch((e) => {
|
||||||
|
// TODO
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -122,4 +154,6 @@ PageLoginSchool.contextTypes = {
|
||||||
push: React.PropTypes.func.isRequired,
|
push: React.PropTypes.func.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
// eslint-disable-next-line react/forbid-prop-types
|
||||||
|
user: React.PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -43,10 +43,17 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"babel-polyfill": "^6.23.0",
|
"babel-polyfill": "^6.23.0",
|
||||||
|
"body-parser": "^1.17.1",
|
||||||
"express": "^4.14.1",
|
"express": "^4.14.1",
|
||||||
|
"jsonwebtoken": "^7.3.0",
|
||||||
|
"jwk-to-pem": "^1.2.6",
|
||||||
|
"moment": "^2.18.1",
|
||||||
"mysql": "^2.13.0",
|
"mysql": "^2.13.0",
|
||||||
|
"node-fetch": "^1.6.3",
|
||||||
"oidc-client": "^1.3.0-beta.3",
|
"oidc-client": "^1.3.0-beta.3",
|
||||||
"react": "^15.4.2",
|
"react": "^15.4.2",
|
||||||
|
"react-big-calendar": "^0.13.0",
|
||||||
|
"react-css-themr": "^2.0.0",
|
||||||
"react-dom": "^15.4.2",
|
"react-dom": "^15.4.2",
|
||||||
"react-router-dom": "^4.0.0-beta.6",
|
"react-router-dom": "^4.0.0-beta.6",
|
||||||
"react-toolbox": "^2.0.0-beta.7",
|
"react-toolbox": "^2.0.0-beta.7",
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
'postcss-import': {
|
'postcss-import': {
|
||||||
root: __dirname,
|
root: __dirname,
|
||||||
},
|
},
|
||||||
'postcss-mixins': {},
|
'postcss-mixins': {},
|
||||||
'postcss-each': {},
|
'postcss-each': {},
|
||||||
'postcss-cssnext': {}
|
'postcss-cssnext': {
|
||||||
},
|
features: {
|
||||||
};
|
customProperties: {
|
||||||
|
variables: {
|
||||||
|
'color-primary': 'var(--palette-purple-500)',
|
||||||
|
'color-primary-dark': 'var(--palette-purple-700)',
|
||||||
|
'color-accent': 'var(--palette-blue-a200)',
|
||||||
|
'color-accent-dark': 'var(--palette-blue-700)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import { get } from 'https';
|
import { get } from 'https';
|
||||||
import Router from 'express';
|
import Router from 'express';
|
||||||
import { WebError, UnauthenticatedError, NotFoundError } from './errors';
|
import bodyParser from 'body-parser';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import jwkToPem from 'jwk-to-pem';
|
||||||
|
|
||||||
|
import { WebError, UnknownError, UnauthenticatedError, NotFoundError, InvalidCredentialsError, BadRequestError } from './errors';
|
||||||
|
|
||||||
export default class API {
|
export default class API {
|
||||||
constructor(database) {
|
constructor(database) {
|
||||||
|
@ -9,10 +14,14 @@ export default class API {
|
||||||
// Binds
|
// Binds
|
||||||
this.auth = this.auth.bind(this);
|
this.auth = this.auth.bind(this);
|
||||||
|
|
||||||
|
// Router
|
||||||
|
|
||||||
this.router = Router({
|
this.router = Router({
|
||||||
strict: true,
|
strict: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.router.use(bodyParser.json());
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
|
|
||||||
this.router.get('/', (req, res) => {
|
this.router.get('/', (req, res) => {
|
||||||
|
@ -50,21 +59,33 @@ export default class API {
|
||||||
.catch(next);
|
.catch(next);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Login
|
||||||
|
this.router.post('/schools/:school/login', (req, res, next) => {
|
||||||
|
this.checkLogin(req.params.school, req.body)
|
||||||
|
.then((l) => {
|
||||||
|
res.json(l);
|
||||||
|
// Generate and return token
|
||||||
|
})
|
||||||
|
.catch(next);
|
||||||
|
});
|
||||||
|
|
||||||
// Users
|
// Users
|
||||||
this.router.get('/schools/:school/users/', this.auth, (req, res, next) => {
|
this.router.get('/schools/:school/users/', this.auth, (req, res, next) => {
|
||||||
this.database.getUsers(req.params.school)
|
this.database.getUsers(req.params.school)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
res.json(data);
|
res.json(data);
|
||||||
}).catch(next);
|
})
|
||||||
|
.catch(next);
|
||||||
});
|
});
|
||||||
this.router.get('/schools/:school/users/:id', this.auth, (req, res, next) => {
|
this.router.get('/schools/:school/users/:id', this.auth, (req, res, next) => {
|
||||||
this.database.getUser(req.params.school, req.params.id)
|
this.database.getUserWithSchool(req.params.school, req.params.id)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
res.json(Object.assign(data, {
|
res.json(Object.assign(data, {
|
||||||
pwd: undefined,
|
pwd: undefined,
|
||||||
oid_id: undefined,
|
oid_id: undefined,
|
||||||
}));
|
}));
|
||||||
}).catch(next);
|
})
|
||||||
|
.catch(next);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.router.use('/*', (req, res, next) => {
|
this.router.use('/*', (req, res, next) => {
|
||||||
|
@ -76,6 +97,9 @@ export default class API {
|
||||||
auth(req, res, next) {
|
auth(req, res, next) {
|
||||||
// res.end('not implemented');
|
// res.end('not implemented');
|
||||||
if (this.validate(req.get('FakeAuth'))) {
|
if (this.validate(req.get('FakeAuth'))) {
|
||||||
|
req.user = {
|
||||||
|
id: req.get('FakeID'),
|
||||||
|
};
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
return next(new UnauthenticatedError());
|
return next(new UnauthenticatedError());
|
||||||
|
@ -86,15 +110,59 @@ export default class API {
|
||||||
return !!token;
|
return !!token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkLogin(school, options) {
|
||||||
|
const checkLoginPassword = async () => {
|
||||||
|
// TODO
|
||||||
|
throw new InvalidCredentialsError('Not implemented');
|
||||||
|
};
|
||||||
|
const checkLoginToken = async (sch, token) => {
|
||||||
|
// do
|
||||||
|
// - get school
|
||||||
|
// then
|
||||||
|
// - fetch school private key
|
||||||
|
// then
|
||||||
|
// - verify jwt
|
||||||
|
const s = await this.database.getSchoolWithAuth(sch);
|
||||||
|
const oidUrl = s.auth[0].oid_meta;
|
||||||
|
if (!oidUrl) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
const o = await fetch(oidUrl).then(res => res.json());
|
||||||
|
const jwksUrl = o.jwks_uri;
|
||||||
|
const k = await fetch(jwksUrl).then(res => res.json());
|
||||||
|
const keys = k.keys;
|
||||||
|
const verified = keys.reduce((a, key) => {
|
||||||
|
try {
|
||||||
|
return jwt.verify(token, jwkToPem(key), {
|
||||||
|
algorithms: ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'],
|
||||||
|
ignoreExpiration: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
if (!verified) {
|
||||||
|
throw new InvalidCredentialsError('Token not verifiable');
|
||||||
|
}
|
||||||
|
return verified;
|
||||||
|
};
|
||||||
|
if (options.type === 'PWD') { // not used
|
||||||
|
return this.database.getUserByEmail(options.email)
|
||||||
|
.then(data => checkLoginPassword(data.pwd_hash, options.pwd) && data);
|
||||||
|
} else if (options.type === 'OID') {
|
||||||
|
return checkLoginToken(school, options.id_token)
|
||||||
|
.then(data => this.database.getUserByEmail(data.upn));
|
||||||
|
}
|
||||||
|
return new BadRequestError();
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
static error(err, req, res, next) {
|
static error(err, req, res, next) {
|
||||||
if (err instanceof WebError) {
|
if (err instanceof WebError) {
|
||||||
err.responseTo(res);
|
err.responseTo(res);
|
||||||
} else if (err) {
|
} else if (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).json({
|
new UnknownError().responseTo(res);
|
||||||
error: 'Unknown error',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,13 +33,13 @@ export default class Database {
|
||||||
id: results[0].id,
|
id: results[0].id,
|
||||||
name: results[0].name,
|
name: results[0].name,
|
||||||
domain: results[0].domain,
|
domain: results[0].domain,
|
||||||
auth: results.map(r => Object.assign(r, {
|
auth: results[0].type ? results.map(r => Object.assign(r, {
|
||||||
school: undefined,
|
school: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
domain: undefined,
|
domain: undefined,
|
||||||
})),
|
})) : [],
|
||||||
}))
|
}))
|
||||||
.catch(err => err.withNoun('School'));
|
.catch(err => Promise.reject(err.withNoun('School')));
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
@ -50,7 +50,17 @@ export default class Database {
|
||||||
WHERE user.school = ?
|
WHERE user.school = ?
|
||||||
`, [school]);
|
`, [school]);
|
||||||
}
|
}
|
||||||
async getUser(school, id) {
|
async getUser(id) {
|
||||||
|
return this.query(`
|
||||||
|
SELECT *
|
||||||
|
FROM user
|
||||||
|
WHERE user.id = ?
|
||||||
|
`, [id], {
|
||||||
|
single: true,
|
||||||
|
})
|
||||||
|
.catch(err => Promise.reject(err.withNoun('User')));
|
||||||
|
}
|
||||||
|
async getUserWithSchool(school, id) {
|
||||||
return this.query(`
|
return this.query(`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM user
|
FROM user
|
||||||
|
@ -59,7 +69,18 @@ export default class Database {
|
||||||
`, [school, id], {
|
`, [school, id], {
|
||||||
single: true,
|
single: true,
|
||||||
})
|
})
|
||||||
.catch(err => err.withNoun('User'));
|
.catch(err => Promise.reject(err.withNoun('User')));
|
||||||
|
}
|
||||||
|
async getUserByEmail(email) {
|
||||||
|
// assumes unique email
|
||||||
|
return this.query(`
|
||||||
|
SELECT *
|
||||||
|
FROM user
|
||||||
|
WHERE user.email = ?
|
||||||
|
`, [email], {
|
||||||
|
single: true,
|
||||||
|
})
|
||||||
|
.catch(err => Promise.reject(err.withNoun('User')));
|
||||||
}
|
}
|
||||||
|
|
||||||
query(query, values, options = {}) {
|
query(query, values, options = {}) {
|
||||||
|
|
|
@ -11,6 +11,15 @@ export class WebError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UnknownError extends WebError {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
this.message = 'Unknown error';
|
||||||
|
this.code = 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class NotFoundError extends WebError {
|
export class NotFoundError extends WebError {
|
||||||
constructor(noun = 'Resource') {
|
constructor(noun = 'Resource') {
|
||||||
super();
|
super();
|
||||||
|
@ -32,3 +41,21 @@ export class UnauthenticatedError extends WebError {
|
||||||
this.code = 403;
|
this.code = 403;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class InvalidCredentialsError extends WebError {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
this.message = 'Invalid credentials';
|
||||||
|
this.code = 403;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BadRequestError extends WebError {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
this.message = 'Bad request';
|
||||||
|
this.code = 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue