diff --git a/.gitignore b/.gitignore index a93fed2..1ab9435 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ dist/ # Temporary files oid_settings.json nginx.conf +sample_data.sql diff --git a/app/assets/core.css b/app/assets/core.css deleted file mode 100644 index e69de29..0000000 diff --git a/app/components/button.jsx b/app/components/button.jsx deleted file mode 100644 index fab9e07..0000000 --- a/app/components/button.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/prefer-stateless-function -export default class Button extends React.Component { - render() { - return ( - - ); - } -} - -Button.propTypes = { - children: React.PropTypes.node.isRequired, - onClick: React.PropTypes.func, - color: React.PropTypes.string, -}; - -Button.defaultProps = { - onClick: () => {}, - color: 'primary', -}; diff --git a/app/components/input.jsx b/app/components/input.jsx deleted file mode 100644 index 5d924c6..0000000 --- a/app/components/input.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/prefer-stateless-function -export default class Input extends React.Component { - render() { - return ( - - ); - } -} - -Input.propTypes = { - type: React.PropTypes.string, - placeholder: React.PropTypes.string, - value: React.PropTypes.string, -}; - -Input.defaultProps = { - type: 'text', - placeholder: '', - value: '', -}; diff --git a/app/components/layouts/column.jsx b/app/components/layouts/column.jsx deleted file mode 100644 index 9ba75c7..0000000 --- a/app/components/layouts/column.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/prefer-stateless-function -export default class Column extends React.Component { - constructor() { - super(); - this.className = this.className.bind(this); - } - - className() { - if (this.props.width) { - if (this.props.breakpoint) { - return `col-${this.props.breakpoint}-${this.props.width}`; - } - return `col-${this.props.width}`; - } - return 'col'; - } - - render() { - return ( -
- { this.props.children } -
- ); - } -} - -Column.propTypes = { - children: React.PropTypes.node.isRequired, - width: React.PropTypes.number, - breakpoint: React.PropTypes.string, -}; - -Column.defaultProps = { - width: null, - breakpoint: null, -}; diff --git a/app/components/layouts/container.jsx b/app/components/layouts/container.jsx deleted file mode 100644 index 9ee0c67..0000000 --- a/app/components/layouts/container.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/prefer-stateless-function -export default class Container extends React.Component { - render() { - return ( -
- { this.props.children } -
- ); - } -} - -Container.propTypes = { - children: React.PropTypes.node.isRequired, -}; diff --git a/app/components/layouts/row.jsx b/app/components/layouts/row.jsx deleted file mode 100644 index aded209..0000000 --- a/app/components/layouts/row.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; - -// eslint-disable-next-line react/prefer-stateless-function -export default class Row extends React.Component { - render() { - return ( -
- { this.props.children } -
- ); - } -} - -Row.propTypes = { - children: React.PropTypes.node.isRequired, -}; - diff --git a/app/index.html b/app/index.html index 7f6808d..10a63b4 100644 --- a/app/index.html +++ b/app/index.html @@ -1,13 +1,13 @@ - + Chronos - + + +
- - - + diff --git a/app/index.jsx b/app/index.jsx index bfd1769..5191415 100644 --- a/app/index.jsx +++ b/app/index.jsx @@ -1,18 +1,24 @@ +import 'babel-polyfill'; + import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter as Router, Route } from 'react-router-dom'; -import Navigation from './navigation'; +// eslint-disable-next-line no-unused-vars +import * as oidc from 'oidc-client'; + +import LayoutMain from './layouts/main'; +import PageHome from './pages/home'; import PageLogin from './pages/login'; -import PageMain from './pages/main'; +import PageLoginSchool from './pages/login_school'; ReactDOM.render( -
- - + + -
+ +
, document.getElementById('root'), ); diff --git a/app/layouts/main.jsx b/app/layouts/main.jsx new file mode 100644 index 0000000..dcb69f4 --- /dev/null +++ b/app/layouts/main.jsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import { Layout, NavDrawer, Panel, AppBar, Navigation, Link, List, ListItem } from 'react-toolbox'; + +export default class LayoutMain extends React.Component { + constructor(props) { + super(props); + this.state = { + drawerActive: false, + }; + + this.toggleDrawerActive = this.toggleDrawerActive.bind(this); + } + + toggleDrawerActive() { + this.setState({ + drawerActive: !this.state.drawerActive, + }); + } + + render() { + return ( + + +
+ {this.props.user ? `Hello, ${this.props.user.name}!` : 'Not logged in'} +
+ + {this.props.user ? this.context.router.history.push('/')} /> : null} + {this.props.user ? : null} + + + + +
+ + + + this.context.router.history.push('/login')} /> + + + + {this.props.children} + +
+ ); + } +} + +LayoutMain.defaultProps = { + children: null, + user: null, +}; + +LayoutMain.propTypes = { + children: React.PropTypes.node, + user: React.PropTypes.shape({ + name: React.PropTypes.string, + }), +}; + +LayoutMain.contextTypes = { + router: React.PropTypes.shape({ + history: React.PropTypes.shape({ + push: React.PropTypes.func.isRequired, + }).isRequired, + }).isRequired, +}; + diff --git a/app/navigation.jsx b/app/navigation.jsx deleted file mode 100644 index 7b6af0d..0000000 --- a/app/navigation.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; - -import Container from './components/layouts/container'; -import Button from './components/button'; - -// eslint-disable-next-line react/prefer-stateless-function -export default class Navigation extends React.Component { - render() { - return ( - - ); - } -} diff --git a/app/pages/home.jsx b/app/pages/home.jsx new file mode 100644 index 0000000..ee4c8e6 --- /dev/null +++ b/app/pages/home.jsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import { Card, CardTitle, CardText, CardActions, Button } from 'react-toolbox'; + +// eslint-disable-next-line react/prefer-stateless-function +export default class PageHome extends React.Component { + constructor(props) { + super(props); + this.state = { + cards: [ + { key: 0, title: 'Math Test', description: 'Meow' }, + ], + }; + } + + render() { + return ( +
+ {this.state.cards.map(card => + + + {card.description} + +
+ ); + } +} diff --git a/app/pages/login.jsx b/app/pages/login.jsx index 315f5e0..02f979d 100644 --- a/app/pages/login.jsx +++ b/app/pages/login.jsx @@ -1,17 +1,62 @@ import React from 'react'; -import Container from '../components/layouts/container'; -import Button from '../components/button'; +import { Dropdown } from 'react-toolbox'; // eslint-disable-next-line react/prefer-stateless-function export default class PageLogin extends React.Component { + constructor(props) { + super(props); + this.state = { + schools: [], + school: null, + }; + + this.changeSchool = this.changeSchool.bind(this); + + this.fetchSchools(); + } + + async fetchSchools() { + return fetch('/api/v1/schools') + .then(data => data.json()) + .then((data) => { + this.setState({ + schools: data.map(s => ({ value: s.id, label: s.name })), + }); + }) + .catch((err) => { + console.error(err); + }); + } + + changeSchool(school) { + this.setState({ + school, + }); + this.context.router.history.push(`/login/${school}`); + } + render() { return ( - - - +
+

+ Login +

+ +
); } } + +PageLogin.contextTypes = { + router: React.PropTypes.shape({ + history: React.PropTypes.shape({ + push: React.PropTypes.func.isRequired, + }).isRequired, + }).isRequired, +}; diff --git a/app/pages/login_school.jsx b/app/pages/login_school.jsx new file mode 100644 index 0000000..87d5b59 --- /dev/null +++ b/app/pages/login_school.jsx @@ -0,0 +1,125 @@ +import React from 'react'; + +import { Input, Button, Snackbar } from 'react-toolbox'; + +import { UserManager } from 'oidc-client'; + +// eslint-disable-next-line react/prefer-stateless-function +export default class PageLoginSchool extends React.Component { + constructor(props) { + super(props); + this.state = { + id: parseInt(props.match.params.id, 10), + school: {}, + openId: false, + email: true, + snackbarActive: false, + }; + + this.loginOpenId = this.loginOpenId.bind(this); + this.loginEmail = this.loginEmail.bind(this); + + this.fetchSchool().then(() => { + if (this.state.openId) { + this.userManager = new UserManager({ + authority: `/api/v1/schools/${this.state.school.id}/oid`, // temp bypass: this.state.school.auth[0].oid_meta, + client_id: this.state.school.auth[0].oid_cid, + redirect_uri: `${window.location.origin}/login/${this.state.school.id}`, + }); + this.userManager.getUser() + .then((user) => { + console.log(user); + if (user) { + this.context.router.history.push('/'); + } + }) + .catch(() => { + + }); + } + }); + } + + async fetchSchool() { + return fetch(`/api/v1/schools/${this.state.id}`) + .then(data => data.json()) + .then((data) => { + this.setState({ + school: data, + openId: !!data.auth[0], + }); + }) + .catch((err) => { + console.error(err); + }); + } + + loginOpenId() { + this.userManager.signinRedirect(); + } + + // eslint-disable-next-line class-methods-use-this + loginEmail() { + console.error('Not implemented'); + } + + handleSnackbarTimeout() { + this.setState({ snackbarActive: false }); + } + + render() { + return ( +
+ {this.state.openId && +
+ ); + } +} + +PageLoginSchool.propTypes = { + match: React.PropTypes.shape({ + params: React.PropTypes.shape({ + id: React.PropTypes.string.isRequired, + }).isRequired, + }).isRequired, +}; + +PageLoginSchool.contextTypes = { + router: React.PropTypes.shape({ + history: React.PropTypes.shape({ + push: React.PropTypes.func.isRequired, + }).isRequired, + }).isRequired, +}; diff --git a/app/pages/main.jsx b/app/pages/main.jsx deleted file mode 100644 index 6e40081..0000000 --- a/app/pages/main.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; - -import Container from '../components/layouts/container'; -import Row from '../components/layouts/row'; -import Column from '../components/layouts/column'; - -// eslint-disable-next-line react/prefer-stateless-function -export default class PageMain extends React.Component { - render() { - return ( - - - -

Hello, world!

-
- -

Hello, world!

-
-
-
- ); - } -} diff --git a/server/api.js b/server/api.js index abd3e73..99c2afd 100644 --- a/server/api.js +++ b/server/api.js @@ -1,5 +1,6 @@ +import { get } from 'https'; import Router from 'express'; -import { WebError, UnauthenticatedError } from './errors'; +import { WebError, UnauthenticatedError, NotFoundError } from './errors'; export default class API { constructor(database) { @@ -23,13 +24,30 @@ export default class API { this.database.getSchools() .then((data) => { res.json(data); - }).catch(next); + }) + .catch(next); }); this.router.get('/schools/:school', (req, res, next) => { - this.database.getSchool(req.params.school) + this.database.getSchoolWithAuth(req.params.school) .then((data) => { - res.json(data); - }).catch(next); + res.json(Object.assign(data, { + auth: data.auth.map(a => Object.assign(a, { oid_csecret: undefined })), + })); + }) + .catch(next); + }); + this.router.get('/schools/:school/oid/.well-known/openid-configuration', (req, res, next) => { + this.database.getSchoolWithAuth(req.params.school) + .then((data) => { + // assume auth[0] exists + const url = data.auth[0].oid_meta; + if (url) { + get(url, (d) => { + d.pipe(res); + }); + } + }) + .catch(next); }); // Users @@ -42,11 +60,16 @@ export default class API { this.router.get('/schools/:school/users/:id', this.auth, (req, res, next) => { this.database.getUser(req.params.school, req.params.id) .then((data) => { - res.json(data); + res.json(Object.assign(data, { + pwd: undefined, + oid_id: undefined, + })); }).catch(next); }); - // this.router.all('/*', this.auth); + this.router.use('/*', (req, res, next) => { + next(new NotFoundError()); + }); this.router.use(API.error); } diff --git a/server/database.js b/server/database.js index 3a72921..40e272e 100644 --- a/server/database.js +++ b/server/database.js @@ -19,7 +19,7 @@ export default class Database { FROM school `); } - async getSchool(id) { + async getSchoolWithAuth(id) { return this.query(` SELECT * FROM auth RIGHT JOIN school @@ -36,7 +36,6 @@ export default class Database { school: undefined, name: undefined, domain: undefined, - oid_csecret: undefined, })), })) .catch(err => err.withNoun('School')); @@ -52,12 +51,12 @@ export default class Database { } async getUser(school, id) { return this.query(` - SELECT id, name, email, role + SELECT * FROM user WHERE user.school = ? AND user.id = ? `, [school, id], { - required: true, + single: true, }) .catch(err => err.withNoun('User')); } @@ -68,10 +67,14 @@ export default class Database { this.connection.query(query, values, (err, results, fields) => { if (err) { reject(err); - } else if (options.required === true && results.length < 1) { + } else if ((options.required || options.single) && results.length < 1) { reject(new NotFoundError()); } - resolve(results, fields); + if (options.single) { + resolve(results[0], fields); + } else { + resolve(results, fields); + } }); }); } @@ -90,9 +93,10 @@ export default class Database { // database is up-to-date return true; } else if (semver.satisfies(oldVersion, '~0')) { // lmao forces database migration + return true; // database needs to be updated - return this.query(`DROP DATABASE ${DATABASE}`) - .then(() => this.checkAndMigrate()); + // return this.query(`DROP DATABASE ${DATABASE}`) + // .then(() => this.checkAndMigrate()); } // database does not exist @@ -205,12 +209,16 @@ export default class Database { VALUES ('version', '${getVersion()}') `); - // TODO: Build admin interface to add schools + // TODO: Build admin interface to add schools and school owner // add first school const firstSchool = await this.query(` INSERT INTO school (name, domain) VALUES (?, ?) `, ['NUS High School', 'nushigh.edu.sg']); + await this.query(` + INSERT INTO user (school, name, email, pwd_hash, role) + VALUES (?, ?, ?, ?, ?) + `, [firstSchool.insertId, 'Ambrose Chua', 'h1310031@nushigh.edu.sg', '', 'OWN']); // eslint-disable-next-line global-require const fs = require('fs'); diff --git a/server/index.js b/server/index.js index d9af39b..bc582a4 100644 --- a/server/index.js +++ b/server/index.js @@ -11,7 +11,14 @@ const database = new Database({ }); const api = new API(database); -app.use('/', express.static(path.join(__dirname, '..', 'app'))); +// API mount app.use('/api/v1', api.router); +// API fallback +app.use('/api', (req, res) => res.end('API')); -app.listen(8080); +// Assets +app.use('/', express.static(path.join(__dirname, '..', 'app'))); +// Pages +app.get('/*', (req, res) => res.sendFile(path.join(__dirname, '..', 'app', 'index.html'))); + +app.listen(process.env.PORT || 8080);