Dump of changes
parent
d8b0554682
commit
35408b43e2
|
@ -9,4 +9,4 @@ dist/
|
|||
# Temporary files
|
||||
oid_settings.json
|
||||
nginx.conf
|
||||
sample_data.sql
|
||||
sample_*.sql
|
||||
|
|
93
Gruntfile.js
93
Gruntfile.js
|
@ -7,7 +7,7 @@ module.exports = function (grunt) {
|
|||
entry: __dirname + '/app/index.jsx',
|
||||
output: {
|
||||
filename: 'bundle.js',
|
||||
path: __dirname + '/dist/app/'
|
||||
path: __dirname + '/dist/app/',
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx'],
|
||||
|
@ -25,15 +25,15 @@ module.exports = function (grunt) {
|
|||
presets: [
|
||||
['env', {
|
||||
targets: {
|
||||
browsers: ['last 2 versions']
|
||||
browsers: ['last 2 versions'],
|
||||
},
|
||||
modules: false
|
||||
modules: false,
|
||||
}],
|
||||
'react'
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
'react',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
|
@ -45,17 +45,17 @@ module.exports = function (grunt) {
|
|||
sourceMap: true,
|
||||
modules: true,
|
||||
importLoaders: 1,
|
||||
localIdentName: '[name]--[local]--[hash:base64:8]'
|
||||
}
|
||||
localIdentName: '[name]--[local]--[hash:base64:8]',
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
loader: 'postcss-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
copy: {
|
||||
app: {
|
||||
|
@ -64,34 +64,37 @@ module.exports = function (grunt) {
|
|||
expand: true,
|
||||
cwd: __dirname + '/app/',
|
||||
src: ['index.html'],
|
||||
dest: __dirname + '/dist/app/'
|
||||
dest: __dirname + '/dist/app/',
|
||||
},
|
||||
{
|
||||
expand: true,
|
||||
cwd: __dirname + '/app/',
|
||||
src: ['assets/**'],
|
||||
dest: __dirname + '/dist/app/assets/'
|
||||
dest: __dirname + '/dist/app/assets/',
|
||||
},
|
||||
{
|
||||
expand: true,
|
||||
cwd: __dirname + '/node_modules/oidc-client/',
|
||||
src: ['dist/**'],
|
||||
dest: __dirname + '/dist/app/assets/oidc-client/'
|
||||
}
|
||||
]
|
||||
}
|
||||
dest: __dirname + '/dist/app/assets/oidc-client/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
babel: {
|
||||
options: {
|
||||
sourceMap: true,
|
||||
presets: [
|
||||
['env', {
|
||||
[
|
||||
'env',
|
||||
{
|
||||
targets: {
|
||||
node: 'current'
|
||||
node: 'current',
|
||||
},
|
||||
modules: 'commonjs'
|
||||
}]
|
||||
]
|
||||
modules: 'commonjs',
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
server: {
|
||||
files: [
|
||||
|
@ -99,10 +102,10 @@ module.exports = function (grunt) {
|
|||
expand: true,
|
||||
cwd: __dirname + '/server/',
|
||||
src: ['**/*.js'],
|
||||
dest: __dirname + '/dist/server/'
|
||||
}
|
||||
]
|
||||
}
|
||||
dest: __dirname + '/dist/server/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
eslint: {
|
||||
app: {
|
||||
|
@ -111,9 +114,9 @@ module.exports = function (grunt) {
|
|||
expand: true,
|
||||
cwd: __dirname + '/app/',
|
||||
src: ['**/*.js*'],
|
||||
dest: __dirname + '/dist/app/'
|
||||
}
|
||||
]
|
||||
dest: __dirname + '/dist/app/',
|
||||
},
|
||||
],
|
||||
},
|
||||
server: {
|
||||
files: [
|
||||
|
@ -121,27 +124,27 @@ module.exports = function (grunt) {
|
|||
expand: true,
|
||||
cwd: __dirname + '/server/',
|
||||
src: ['**/*.js'],
|
||||
dest: __dirname + '/dist/server/'
|
||||
}
|
||||
]
|
||||
}
|
||||
dest: __dirname + '/dist/server/',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
app: {
|
||||
files: '**/*.js*',
|
||||
tasks: ['app'],
|
||||
options: {
|
||||
cwd: __dirname + '/app/'
|
||||
}
|
||||
cwd: __dirname + '/app/',
|
||||
},
|
||||
},
|
||||
server: {
|
||||
files: '**/*.js',
|
||||
tasks: ['server'],
|
||||
options: {
|
||||
cwd: __dirname + '/server/'
|
||||
}
|
||||
}
|
||||
}
|
||||
cwd: __dirname + '/server/',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
grunt.loadNpmTasks('grunt-webpack');
|
||||
|
|
|
@ -31,4 +31,5 @@ A school event planner and timetable
|
|||
## Security Pitfalls
|
||||
|
||||
- Auth mechanism not verified
|
||||
- Verification of OID tokens is done by upn being the email address
|
||||
- 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>
|
||||
<meta charset="UTF-8" />
|
||||
<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/css?family=Roboto:300,400,500,700" rel="stylesheet" />
|
||||
<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 ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router, Route } from 'react-router-dom';
|
||||
|
||||
// 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 PageLoginSchool from './pages/login_school';
|
||||
import App from './components/app';
|
||||
|
||||
ReactDOM.render(
|
||||
<Router>
|
||||
<LayoutMain>
|
||||
<Route exact path="/" component={PageHome} />
|
||||
<Route path="/login" component={PageLogin} />
|
||||
<Route path="/login/:id" component={PageLoginSchool} />
|
||||
</LayoutMain>
|
||||
</Router>,
|
||||
<App />,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
|
|
|
@ -14,7 +14,7 @@ export default class LayoutMain extends React.Component {
|
|||
|
||||
toggleDrawerActive() {
|
||||
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}
|
||||
>
|
||||
<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>
|
||||
<List selectable ripple>
|
||||
{this.props.user ? <ListItem caption="Home" onClick={() => this.context.router.history.push('/')} /> : null}
|
||||
{this.props.user ? <ListItem caption="Logout" /> : null}
|
||||
{this.context.user.email ? <ListItem caption="Home" onClick={() => this.context.router.history.push('/')} /> : null}
|
||||
{this.context.user.email ? <ListItem caption="Logout" /> : null}
|
||||
<ListItem caption="Help" />
|
||||
<ListItem caption="About" />
|
||||
<ListItem to="https://github.com/ambrosechua/chronos" caption="GitHub" />
|
||||
|
@ -40,8 +43,7 @@ export default class LayoutMain extends React.Component {
|
|||
<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" />
|
||||
<Link label="Login" onClick={() => this.context.router.history.push('/login')} style={{ color: 'var(--color-dark-contrast)' }} />
|
||||
</Navigation>
|
||||
</AppBar>
|
||||
{this.props.children}
|
||||
|
@ -53,14 +55,10 @@ export default class LayoutMain extends React.Component {
|
|||
|
||||
LayoutMain.defaultProps = {
|
||||
children: null,
|
||||
user: null,
|
||||
};
|
||||
|
||||
LayoutMain.propTypes = {
|
||||
children: React.PropTypes.node,
|
||||
user: React.PropTypes.shape({
|
||||
name: React.PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
LayoutMain.contextTypes = {
|
||||
|
@ -69,5 +67,7 @@ LayoutMain.contextTypes = {
|
|||
push: React.PropTypes.func.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 { 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
|
||||
export default class PageHome extends React.Component {
|
||||
|
@ -10,22 +15,47 @@ export default class PageHome extends React.Component {
|
|||
cards: [
|
||||
{ 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() {
|
||||
return (
|
||||
<main>
|
||||
<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" />
|
||||
<Button label="Edit" accent />
|
||||
</CardActions>
|
||||
</Card>,
|
||||
)}
|
||||
</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';
|
||||
|
||||
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
|
||||
export default class PageLoginSchool extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -26,16 +41,33 @@ export default class PageLoginSchool extends React.Component {
|
|||
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(() => {
|
||||
|
||||
const params = getParams(window.location.hash);
|
||||
if (params.id_token) {
|
||||
// TODO: check auth by sending request to server
|
||||
// TODO: auth endpoint should return user information
|
||||
// TODO: use user information here:
|
||||
const method = 'POST';
|
||||
const headers = new Headers();
|
||||
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,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
user: React.PropTypes.object.isRequired,
|
||||
};
|
||||
|
|
|
@ -43,10 +43,17 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"babel-polyfill": "^6.23.0",
|
||||
"body-parser": "^1.17.1",
|
||||
"express": "^4.14.1",
|
||||
"jsonwebtoken": "^7.3.0",
|
||||
"jwk-to-pem": "^1.2.6",
|
||||
"moment": "^2.18.1",
|
||||
"mysql": "^2.13.0",
|
||||
"node-fetch": "^1.6.3",
|
||||
"oidc-client": "^1.3.0-beta.3",
|
||||
"react": "^15.4.2",
|
||||
"react-big-calendar": "^0.13.0",
|
||||
"react-css-themr": "^2.0.0",
|
||||
"react-dom": "^15.4.2",
|
||||
"react-router-dom": "^4.0.0-beta.6",
|
||||
"react-toolbox": "^2.0.0-beta.7",
|
||||
|
|
|
@ -5,6 +5,17 @@ module.exports = {
|
|||
},
|
||||
'postcss-mixins': {},
|
||||
'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 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 {
|
||||
constructor(database) {
|
||||
|
@ -9,10 +14,14 @@ export default class API {
|
|||
// Binds
|
||||
this.auth = this.auth.bind(this);
|
||||
|
||||
// Router
|
||||
|
||||
this.router = Router({
|
||||
strict: true,
|
||||
});
|
||||
|
||||
this.router.use(bodyParser.json());
|
||||
|
||||
// Routes
|
||||
|
||||
this.router.get('/', (req, res) => {
|
||||
|
@ -50,21 +59,33 @@ export default class API {
|
|||
.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
|
||||
this.router.get('/schools/:school/users/', this.auth, (req, res, next) => {
|
||||
this.database.getUsers(req.params.school)
|
||||
.then((data) => {
|
||||
res.json(data);
|
||||
}).catch(next);
|
||||
})
|
||||
.catch(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) => {
|
||||
res.json(Object.assign(data, {
|
||||
pwd: undefined,
|
||||
oid_id: undefined,
|
||||
}));
|
||||
}).catch(next);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
this.router.use('/*', (req, res, next) => {
|
||||
|
@ -76,6 +97,9 @@ export default class API {
|
|||
auth(req, res, next) {
|
||||
// res.end('not implemented');
|
||||
if (this.validate(req.get('FakeAuth'))) {
|
||||
req.user = {
|
||||
id: req.get('FakeID'),
|
||||
};
|
||||
return next();
|
||||
}
|
||||
return next(new UnauthenticatedError());
|
||||
|
@ -86,15 +110,59 @@ export default class API {
|
|||
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
|
||||
static error(err, req, res, next) {
|
||||
if (err instanceof WebError) {
|
||||
err.responseTo(res);
|
||||
} else if (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({
|
||||
error: 'Unknown error',
|
||||
});
|
||||
new UnknownError().responseTo(res);
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
|
|
@ -33,13 +33,13 @@ export default class Database {
|
|||
id: results[0].id,
|
||||
name: results[0].name,
|
||||
domain: results[0].domain,
|
||||
auth: results.map(r => Object.assign(r, {
|
||||
auth: results[0].type ? results.map(r => Object.assign(r, {
|
||||
school: undefined,
|
||||
name: undefined,
|
||||
domain: undefined,
|
||||
})),
|
||||
})) : [],
|
||||
}))
|
||||
.catch(err => err.withNoun('School'));
|
||||
.catch(err => Promise.reject(err.withNoun('School')));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
|
@ -50,7 +50,17 @@ export default class Database {
|
|||
WHERE user.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(`
|
||||
SELECT *
|
||||
FROM user
|
||||
|
@ -59,7 +69,18 @@ export default class Database {
|
|||
`, [school, id], {
|
||||
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 = {}) {
|
||||
|
|
|
@ -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 {
|
||||
constructor(noun = 'Resource') {
|
||||
super();
|
||||
|
@ -32,3 +41,21 @@ export class UnauthenticatedError extends WebError {
|
|||
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