Begin some React Toolbox UI
parent
d1972049ae
commit
101e95599e
|
@ -9,3 +9,4 @@ dist/
|
||||||
# Temporary files
|
# Temporary files
|
||||||
oid_settings.json
|
oid_settings.json
|
||||||
nginx.conf
|
nginx.conf
|
||||||
|
sample_data.sql
|
||||||
|
|
|
@ -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 className={`btn btn-${this.props.color}`} onClick={this.props.onClick}>
|
|
||||||
{ this.props.children }
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Button.propTypes = {
|
|
||||||
children: React.PropTypes.node.isRequired,
|
|
||||||
onClick: React.PropTypes.func,
|
|
||||||
color: React.PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
Button.defaultProps = {
|
|
||||||
onClick: () => {},
|
|
||||||
color: 'primary',
|
|
||||||
};
|
|
|
@ -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
|
|
||||||
type={this.props.type}
|
|
||||||
placeholder={this.props.placeholder}
|
|
||||||
className="form-control"
|
|
||||||
value={this.props.value}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Input.propTypes = {
|
|
||||||
type: React.PropTypes.string,
|
|
||||||
placeholder: React.PropTypes.string,
|
|
||||||
value: React.PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
Input.defaultProps = {
|
|
||||||
type: 'text',
|
|
||||||
placeholder: '',
|
|
||||||
value: '',
|
|
||||||
};
|
|
|
@ -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 (
|
|
||||||
<div className={this.className()}>
|
|
||||||
{ this.props.children }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column.propTypes = {
|
|
||||||
children: React.PropTypes.node.isRequired,
|
|
||||||
width: React.PropTypes.number,
|
|
||||||
breakpoint: React.PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
Column.defaultProps = {
|
|
||||||
width: null,
|
|
||||||
breakpoint: 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 (
|
|
||||||
<div className="container">
|
|
||||||
{ this.props.children }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Container.propTypes = {
|
|
||||||
children: React.PropTypes.node.isRequired,
|
|
||||||
};
|
|
|
@ -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 (
|
|
||||||
<div className="row">
|
|
||||||
{ this.props.children }
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row.propTypes = {
|
|
||||||
children: React.PropTypes.node.isRequired,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<title>Chronos</title>
|
<title>Chronos</title>
|
||||||
<link rel="stylesheet" href="assets/bootstrap/dist/css/bootstrap.min.css" />
|
<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" />
|
||||||
|
<style>*, *:before, *:after { box-sizing: border-box; } html, body { margin: 0; padding: 0; }</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root">
|
<div id="root">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<script src="assets/oidc-client/dist/oidc-client.min.js"></script>
|
<script src="/bundle.js" async></script>
|
||||||
<script src="assets/bootstrap/dist/js/bootstrap.min.js"></script>
|
|
||||||
<script src="bundle.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,18 +1,24 @@
|
||||||
|
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';
|
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 PageLogin from './pages/login';
|
||||||
import PageMain from './pages/main';
|
import PageLoginSchool from './pages/login_school';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Router>
|
<Router>
|
||||||
<div>
|
<LayoutMain>
|
||||||
<Navigation />
|
<Route exact path="/" component={PageHome} />
|
||||||
<Route exact path="/" component={PageMain} />
|
|
||||||
<Route path="/login" component={PageLogin} />
|
<Route path="/login" component={PageLogin} />
|
||||||
</div>
|
<Route path="/login/:id" component={PageLoginSchool} />
|
||||||
|
</LayoutMain>
|
||||||
</Router>,
|
</Router>,
|
||||||
document.getElementById('root'),
|
document.getElementById('root'),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 (
|
||||||
|
<Layout>
|
||||||
|
<NavDrawer
|
||||||
|
active={this.state.drawerActive}
|
||||||
|
permanentAt="md"
|
||||||
|
onOverlayClick={this.toggleDrawerActive}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: '1.2em' }}>
|
||||||
|
{this.props.user ? `Hello, ${this.props.user.name}!` : 'Not logged in'}
|
||||||
|
</div>
|
||||||
|
<List selectable ripple>
|
||||||
|
{this.props.user ? <ListItem caption="Home" onClick={() => this.context.router.history.push('/')} /> : null}
|
||||||
|
{this.props.user ? <ListItem caption="Logout" /> : null}
|
||||||
|
<ListItem caption="Help" />
|
||||||
|
<ListItem caption="About" />
|
||||||
|
<ListItem to="https://github.com/ambrosechua/chronos" caption="GitHub" />
|
||||||
|
</List>
|
||||||
|
</NavDrawer>
|
||||||
|
<Panel>
|
||||||
|
<AppBar title="Chronos" leftIcon="menu" onLeftIconClick={this.toggleDrawerActive}>
|
||||||
|
<Navigation type="horizontal">
|
||||||
|
<Link label="Inbox" icon="inbox" onClick={() => this.context.router.history.push('/login')} />
|
||||||
|
<Link active label="Profile" icon="person" />
|
||||||
|
</Navigation>
|
||||||
|
</AppBar>
|
||||||
|
{this.props.children}
|
||||||
|
</Panel>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
|
@ -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 (
|
|
||||||
<nav className="navbar navbar-inverse bg-inverse sticky-top">
|
|
||||||
<Container>
|
|
||||||
<div className="d-flex flex-row">
|
|
||||||
<span className="navbar-brand">Chronos</span>
|
|
||||||
<ul className="navbar-nav mr-auto">
|
|
||||||
<li className="nav-item">
|
|
||||||
<Link className="nav-link" to="/login">Login</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div className="form-inline">
|
|
||||||
<Button color="secondary" onClick={() => alert('test')}>Logout</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 (
|
||||||
|
<div style={{ padding: '1em' }}>
|
||||||
|
{this.state.cards.map(card =>
|
||||||
|
<Card key={card.key} style={{ margin: '1em', width: 'auto' }}>
|
||||||
|
<CardTitle title={card.title} />
|
||||||
|
<CardText>{card.description}</CardText>
|
||||||
|
<CardActions>
|
||||||
|
<Button label="Edit" />
|
||||||
|
</CardActions>
|
||||||
|
</Card>,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,62 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Container from '../components/layouts/container';
|
import { Dropdown } from 'react-toolbox';
|
||||||
import Button from '../components/button';
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/prefer-stateless-function
|
// eslint-disable-next-line react/prefer-stateless-function
|
||||||
export default class PageLogin extends React.Component {
|
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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<section style={{ padding: '1em', margin: '0 auto', maxWidth: '480px' }}>
|
||||||
<Button>
|
<h1>
|
||||||
Login with Office365
|
Login
|
||||||
</Button>
|
</h1>
|
||||||
</Container>
|
<Dropdown
|
||||||
|
onChange={this.changeSchool}
|
||||||
|
source={this.state.schools}
|
||||||
|
value={this.state.school}
|
||||||
|
label="School"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PageLogin.contextTypes = {
|
||||||
|
router: React.PropTypes.shape({
|
||||||
|
history: React.PropTypes.shape({
|
||||||
|
push: React.PropTypes.func.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|
|
@ -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 (
|
||||||
|
<section style={{ padding: '1em', margin: '0 auto', maxWidth: '480px' }}>
|
||||||
|
{this.state.openId &&
|
||||||
|
<Button
|
||||||
|
onClick={this.loginOpenId}
|
||||||
|
icon="fingerprint"
|
||||||
|
label="Authenticate with Office365"
|
||||||
|
raised primary style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{this.state.openId && this.state.email &&
|
||||||
|
<div style={{ textAlign: 'center', margin: '1rem 0 0 0' }}>
|
||||||
|
OR
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{this.state.email &&
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
label="Email"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={this.loginEmail}
|
||||||
|
label="Authenticate with email"
|
||||||
|
raised primary style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
<Snackbar
|
||||||
|
onTimeout={this.state.handleSnackbarTimeout}
|
||||||
|
active={this.state.snackbarActive}
|
||||||
|
timeout={2000}
|
||||||
|
label="Not implemented"
|
||||||
|
type="accept"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
|
@ -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 (
|
|
||||||
<Container>
|
|
||||||
<Row>
|
|
||||||
<Column width="3" breakpoint="lg">
|
|
||||||
<h1>Hello, world! </h1>
|
|
||||||
</Column>
|
|
||||||
<Column>
|
|
||||||
<h1>Hello, world! </h1>
|
|
||||||
</Column>
|
|
||||||
</Row>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import { get } from 'https';
|
||||||
import Router from 'express';
|
import Router from 'express';
|
||||||
import { WebError, UnauthenticatedError } from './errors';
|
import { WebError, UnauthenticatedError, NotFoundError } from './errors';
|
||||||
|
|
||||||
export default class API {
|
export default class API {
|
||||||
constructor(database) {
|
constructor(database) {
|
||||||
|
@ -23,13 +24,30 @@ export default class API {
|
||||||
this.database.getSchools()
|
this.database.getSchools()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
res.json(data);
|
res.json(data);
|
||||||
}).catch(next);
|
})
|
||||||
|
.catch(next);
|
||||||
});
|
});
|
||||||
this.router.get('/schools/:school', (req, res, next) => {
|
this.router.get('/schools/:school', (req, res, next) => {
|
||||||
this.database.getSchool(req.params.school)
|
this.database.getSchoolWithAuth(req.params.school)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
res.json(data);
|
res.json(Object.assign(data, {
|
||||||
}).catch(next);
|
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
|
// Users
|
||||||
|
@ -42,11 +60,16 @@ export default class API {
|
||||||
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.getUser(req.params.school, req.params.id)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
res.json(data);
|
res.json(Object.assign(data, {
|
||||||
|
pwd: undefined,
|
||||||
|
oid_id: undefined,
|
||||||
|
}));
|
||||||
}).catch(next);
|
}).catch(next);
|
||||||
});
|
});
|
||||||
|
|
||||||
// this.router.all('/*', this.auth);
|
this.router.use('/*', (req, res, next) => {
|
||||||
|
next(new NotFoundError());
|
||||||
|
});
|
||||||
this.router.use(API.error);
|
this.router.use(API.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default class Database {
|
||||||
FROM school
|
FROM school
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
async getSchool(id) {
|
async getSchoolWithAuth(id) {
|
||||||
return this.query(`
|
return this.query(`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM auth RIGHT JOIN school
|
FROM auth RIGHT JOIN school
|
||||||
|
@ -36,7 +36,6 @@ export default class Database {
|
||||||
school: undefined,
|
school: undefined,
|
||||||
name: undefined,
|
name: undefined,
|
||||||
domain: undefined,
|
domain: undefined,
|
||||||
oid_csecret: undefined,
|
|
||||||
})),
|
})),
|
||||||
}))
|
}))
|
||||||
.catch(err => err.withNoun('School'));
|
.catch(err => err.withNoun('School'));
|
||||||
|
@ -52,12 +51,12 @@ export default class Database {
|
||||||
}
|
}
|
||||||
async getUser(school, id) {
|
async getUser(school, id) {
|
||||||
return this.query(`
|
return this.query(`
|
||||||
SELECT id, name, email, role
|
SELECT *
|
||||||
FROM user
|
FROM user
|
||||||
WHERE user.school = ?
|
WHERE user.school = ?
|
||||||
AND user.id = ?
|
AND user.id = ?
|
||||||
`, [school, id], {
|
`, [school, id], {
|
||||||
required: true,
|
single: true,
|
||||||
})
|
})
|
||||||
.catch(err => err.withNoun('User'));
|
.catch(err => err.withNoun('User'));
|
||||||
}
|
}
|
||||||
|
@ -68,10 +67,14 @@ export default class Database {
|
||||||
this.connection.query(query, values, (err, results, fields) => {
|
this.connection.query(query, values, (err, results, fields) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else if (options.required === true && results.length < 1) {
|
} else if ((options.required || options.single) && results.length < 1) {
|
||||||
reject(new NotFoundError());
|
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
|
// database is up-to-date
|
||||||
return true;
|
return true;
|
||||||
} else if (semver.satisfies(oldVersion, '~0')) { // lmao forces database migration
|
} else if (semver.satisfies(oldVersion, '~0')) { // lmao forces database migration
|
||||||
|
return true;
|
||||||
// database needs to be updated
|
// database needs to be updated
|
||||||
return this.query(`DROP DATABASE ${DATABASE}`)
|
// return this.query(`DROP DATABASE ${DATABASE}`)
|
||||||
.then(() => this.checkAndMigrate());
|
// .then(() => this.checkAndMigrate());
|
||||||
}
|
}
|
||||||
// database does not exist
|
// database does not exist
|
||||||
|
|
||||||
|
@ -205,12 +209,16 @@ export default class Database {
|
||||||
VALUES ('version', '${getVersion()}')
|
VALUES ('version', '${getVersion()}')
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// TODO: Build admin interface to add schools
|
// TODO: Build admin interface to add schools and school owner
|
||||||
// add first school
|
// add first school
|
||||||
const firstSchool = await this.query(`
|
const firstSchool = await this.query(`
|
||||||
INSERT INTO school (name, domain)
|
INSERT INTO school (name, domain)
|
||||||
VALUES (?, ?)
|
VALUES (?, ?)
|
||||||
`, ['NUS High School', 'nushigh.edu.sg']);
|
`, ['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
|
// eslint-disable-next-line global-require
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
|
@ -11,7 +11,14 @@ const database = new Database({
|
||||||
});
|
});
|
||||||
const api = new API(database);
|
const api = new API(database);
|
||||||
|
|
||||||
app.use('/', express.static(path.join(__dirname, '..', 'app')));
|
// API mount
|
||||||
app.use('/api/v1', api.router);
|
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);
|
||||||
|
|
Loading…
Reference in New Issue