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 (
-
-
-
+
);
}
}
+
+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 (
+
+ );
+ }
+}
+
+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);