Another changedump
parent
002b8f5e7a
commit
2ed90533ec
|
@ -125,3 +125,10 @@ export default class AddEventDialog extends React.Component {
|
|||
AddEventDialog.propTypes = {
|
||||
onCancel: React.PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
AddEventDialog.contextTypes = {
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
user: React.PropTypes.object.isRequired,
|
||||
token: React.PropTypes.string,
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Dialog, Input, Autocomplete, Dropdown } from 'react-toolbox';
|
||||
|
||||
export default class AddGroupDialog extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
this.state = {
|
||||
name: '',
|
||||
members: [],
|
||||
users: [],
|
||||
type: 'DEF',
|
||||
types: [ // TODO: define this by school
|
||||
{ value: 'DEF', label: 'Normal' },
|
||||
{ value: 'CCA', label: 'CCA' },
|
||||
{ value: 'MEN', label: 'Mentor Group' },
|
||||
{ value: 'ING', label: 'Intrest Group' },
|
||||
],
|
||||
mentor_level: 0, // TODO: parse from name
|
||||
mentor_year: new Date().getFullYear(),
|
||||
};
|
||||
|
||||
this.handleNameChange = this.handleNameChange.bind(this);
|
||||
this.handleMembersChange = this.handleMembersChange.bind(this);
|
||||
this.handleTypeChange = this.handleTypeChange.bind(this);
|
||||
this.fetchUsers = this.fetchUsers.bind(this);
|
||||
this.addGroup = this.addGroup.bind(this);
|
||||
|
||||
this.actions = [
|
||||
{ label: 'Cancel', onClick: this.props.onCancel, accent: true },
|
||||
{ label: 'Ok', onClick: this.addGroup, accent: true },
|
||||
];
|
||||
|
||||
this.fetchUsers(context);
|
||||
}
|
||||
|
||||
fetchUsers(context) {
|
||||
const headers = new Headers();
|
||||
headers.append('FakeAuth', 'true');
|
||||
headers.append('FakeID', context.user.id);
|
||||
fetch(`/api/v1/schools/${context.user.school}/users`, {
|
||||
headers,
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then((data) => {
|
||||
this.setState({
|
||||
users: data.reduce((a, u) => {
|
||||
const o = {};
|
||||
o[u.id] = u.name;
|
||||
return Object.assign(a, o);
|
||||
}),
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
addGroup() {
|
||||
const method = 'POST';
|
||||
const headers = new Headers();
|
||||
headers.append('FakeAuth', 'true');
|
||||
headers.append('FakeID', this.context.user.id);
|
||||
headers.append('Content-Type', 'application/json');
|
||||
const body = JSON.stringify({
|
||||
name: this.state.name,
|
||||
members: this.state.members,
|
||||
type: this.state.type,
|
||||
mentor_level: this.state.mentor_level,
|
||||
mentor_year: this.state.mentor_year,
|
||||
});
|
||||
fetch(`/api/v1/schools/${this.context.user.school}/groups`, {
|
||||
method, headers, body,
|
||||
})
|
||||
.then(() => {
|
||||
this.props.onDone();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
handleNameChange(value) {
|
||||
this.setState({
|
||||
name: value,
|
||||
});
|
||||
}
|
||||
|
||||
handleMembersChange(value) {
|
||||
this.setState({
|
||||
members: value,
|
||||
});
|
||||
}
|
||||
|
||||
handleTypeChange(value) {
|
||||
this.setState({
|
||||
type: value,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
{...this.props}
|
||||
actions={this.actions}
|
||||
title="Create a new group"
|
||||
> // TODO: make scrollable
|
||||
<Input
|
||||
type="text"
|
||||
label="Name"
|
||||
value={this.state.name}
|
||||
required
|
||||
onChange={this.handleNameChange}
|
||||
/>
|
||||
<Autocomplete
|
||||
keepFocusOnChange
|
||||
direction="down"
|
||||
selectedPosition="below"
|
||||
type="text"
|
||||
label="Add member"
|
||||
hint="Start typing..."
|
||||
suggestionMatch="anywhere"
|
||||
source={this.state.users}
|
||||
value={this.state.members}
|
||||
onChange={this.handleMembersChange}
|
||||
/>
|
||||
<Dropdown
|
||||
auto
|
||||
source={this.state.types}
|
||||
value={this.state.type}
|
||||
onChange={this.handleTypeChange}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddGroupDialog.propTypes = {
|
||||
onCancel: React.PropTypes.func.isRequired,
|
||||
onDone: React.PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
AddGroupDialog.contextTypes = {
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
user: React.PropTypes.object.isRequired,
|
||||
token: React.PropTypes.string,
|
||||
};
|
|
@ -5,6 +5,7 @@ import LayoutMain from '../layouts/main';
|
|||
import PageHome from '../pages/home';
|
||||
import PageLogin from '../pages/login';
|
||||
import PageLoginSchool from '../pages/login_school';
|
||||
import PageGroups from '../pages/groups';
|
||||
|
||||
export default class App extends React.Component {
|
||||
getChildContext() {
|
||||
|
@ -21,6 +22,7 @@ export default class App extends React.Component {
|
|||
<Route exact path="/" component={PageHome} />
|
||||
<Route path="/login" component={PageLogin} />
|
||||
<Route path="/login/:id" component={PageLoginSchool} />
|
||||
<Route path="/groups" component={PageGroups} />
|
||||
</LayoutMain>
|
||||
</Router>
|
||||
);
|
||||
|
|
|
@ -26,24 +26,73 @@ export default class LayoutMain extends React.Component {
|
|||
permanentAt="md"
|
||||
onOverlayClick={this.toggleDrawerActive}
|
||||
>
|
||||
<div style={{ fontSize: '1.2em' }}>
|
||||
<div
|
||||
style={{ fontSize: '1.2em' }}
|
||||
>
|
||||
{this.context.user.email ?
|
||||
<span>
|
||||
Hello, <span title={this.context.user.email}>{this.context.user.name}</span>!
|
||||
</span> : 'Not logged in'}
|
||||
</span>
|
||||
:
|
||||
'Not logged in'
|
||||
}
|
||||
</div>
|
||||
<List selectable ripple>
|
||||
{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" />
|
||||
<List
|
||||
selectable
|
||||
ripple
|
||||
>
|
||||
{this.context.user.email ?
|
||||
[
|
||||
<ListItem
|
||||
key={0}
|
||||
caption="Home"
|
||||
onClick={() => this.context.router.history.push('/')}
|
||||
/>,
|
||||
<ListItem
|
||||
key={1}
|
||||
caption="Groups"
|
||||
onClick={() => this.context.router.history.push('/groups')}
|
||||
/>,
|
||||
]
|
||||
:
|
||||
null
|
||||
}
|
||||
<ListItem
|
||||
caption="Help"
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<ListItem
|
||||
caption="About"
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<ListItem
|
||||
caption="GitHub"
|
||||
to="https://github.com/ambrosechua/chronos"
|
||||
/>
|
||||
</List>
|
||||
</NavDrawer>
|
||||
<Panel>
|
||||
<AppBar title="Chronos" leftIcon="menu" onLeftIconClick={this.toggleDrawerActive}>
|
||||
<Navigation type="horizontal">
|
||||
<Link label="Login" onClick={() => this.context.router.history.push('/login')} style={{ color: 'var(--color-dark-contrast)' }} />
|
||||
<AppBar
|
||||
title="Chronos"
|
||||
leftIcon="menu"
|
||||
onLeftIconClick={this.toggleDrawerActive}
|
||||
>
|
||||
<Navigation
|
||||
type="horizontal"
|
||||
>
|
||||
{this.context.user.email ?
|
||||
<Link
|
||||
style={{ color: 'var(--color-dark-contrast)' }}
|
||||
label="Logout"
|
||||
onClick={() => this.context.router.history.push('/logout')}
|
||||
/>
|
||||
:
|
||||
<Link
|
||||
style={{ color: 'var(--color-dark-contrast)' }}
|
||||
label="Login"
|
||||
onClick={() => this.context.router.history.push('/login')}
|
||||
/>
|
||||
}
|
||||
</Navigation>
|
||||
</AppBar>
|
||||
{this.props.children}
|
||||
|
@ -69,5 +118,6 @@ LayoutMain.contextTypes = {
|
|||
}).isRequired,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
user: React.PropTypes.object.isRequired,
|
||||
token: React.PropTypes.string,
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
import React from 'react';
|
||||
|
||||
import { List, ListItem, Button } from 'react-toolbox';
|
||||
|
||||
import AddGroupDialog from '../components/addgroupdialog';
|
||||
|
||||
export default class PageGroups extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
this.state = {
|
||||
groups: [
|
||||
{ id: 0, name: 'Test' },
|
||||
{ id: 1, name: 'Test 2' },
|
||||
{ id: 2, name: 'Test 3' },
|
||||
{ id: 3, name: 'Test 4' },
|
||||
],
|
||||
addGroupDialogActive: false,
|
||||
};
|
||||
|
||||
this.showAddGroupDialog = this.showAddGroupDialog.bind(this);
|
||||
this.hideAddGroupDialog = this.hideAddGroupDialog.bind(this);
|
||||
this.fetchGroups = this.fetchGroups.bind(this);
|
||||
|
||||
this.fetchGroups(context);
|
||||
}
|
||||
|
||||
async fetchGroups(context = this.context) {
|
||||
return fetch(`/api/v1/schools/${context.user.school}/users/${context.user.id}/groups`, {
|
||||
headers: {
|
||||
FakeAuth: true,
|
||||
FakeID: context.user.id,
|
||||
},
|
||||
})
|
||||
.then(data => data.json())
|
||||
.then((data) => {
|
||||
this.setState({
|
||||
groups: data,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
showAddGroupDialog() {
|
||||
this.setState({
|
||||
addGroupDialogActive: true,
|
||||
});
|
||||
}
|
||||
|
||||
hideAddGroupDialog() {
|
||||
this.setState({
|
||||
addGroupDialogActive: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<main>
|
||||
<List
|
||||
selectable
|
||||
ripple
|
||||
>
|
||||
{this.state.groups.map(group =>
|
||||
<ListItem
|
||||
key={group.id}
|
||||
caption={group.name}
|
||||
onClick={() => this.context.router.history.push(`/groups/${group.id}`)}
|
||||
/>,
|
||||
)}
|
||||
</List>
|
||||
<Button
|
||||
style={{ position: 'fixed', bottom: '1em', right: '1em' }}
|
||||
floating
|
||||
accent
|
||||
icon="add"
|
||||
onClick={this.showAddGroupDialog}
|
||||
/>
|
||||
<AddGroupDialog
|
||||
active={this.state.addGroupDialogActive}
|
||||
onCancel={this.hideAddGroupDialog}
|
||||
onEscKeyDown={this.hideAddGroupDialog}
|
||||
onOverlayClick={this.hideAddGroupDialog}
|
||||
onDone={() => { this.fetchGroups(); this.hideAddGroupDialog(); }}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PageGroups.contextTypes = {
|
||||
router: React.PropTypes.shape({
|
||||
history: React.PropTypes.shape({
|
||||
push: React.PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
user: React.PropTypes.object.isRequired,
|
||||
token: React.PropTypes.string,
|
||||
};
|
||||
|
|
@ -7,7 +7,6 @@ import { Card, CardTitle, CardText, CardActions, Button } from 'react-toolbox';
|
|||
|
||||
import AddEventDialog from '../components/addeventdialog';
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
export default class PageHome extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
|
||||
import { Dropdown } from 'react-toolbox';
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
export default class PageLogin extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
|
|
@ -19,7 +19,6 @@ const getParams = (query) => {
|
|||
}, {});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
export default class PageLoginSchool extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -43,9 +42,6 @@ export default class PageLoginSchool extends React.Component {
|
|||
});
|
||||
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');
|
||||
|
@ -57,6 +53,7 @@ export default class PageLoginSchool extends React.Component {
|
|||
method, headers, body,
|
||||
}).then(async (response) => {
|
||||
if (response.ok) {
|
||||
// TODO: change to setting token
|
||||
Object.assign(this.context.user, await response.json());
|
||||
this.context.router.history.push('/');
|
||||
} else {
|
||||
|
@ -156,4 +153,5 @@ PageLoginSchool.contextTypes = {
|
|||
}).isRequired,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
user: React.PropTypes.object.isRequired,
|
||||
token: React.PropTypes.string,
|
||||
};
|
||||
|
|
|
@ -62,9 +62,9 @@ export default class API {
|
|||
// 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
|
||||
.then((data) => {
|
||||
res.json(data);
|
||||
// TODO: generate and return token instead of user object
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
@ -78,7 +78,7 @@ export default class API {
|
|||
.catch(next);
|
||||
});
|
||||
this.router.get('/schools/:school/users/:id', this.auth, (req, res, next) => {
|
||||
this.database.getUserWithSchool(req.params.school, req.params.id)
|
||||
this.database.getUser(req.params.school, req.params.id)
|
||||
.then((data) => {
|
||||
res.json(Object.assign(data, {
|
||||
pwd: undefined,
|
||||
|
@ -87,6 +87,29 @@ export default class API {
|
|||
})
|
||||
.catch(next);
|
||||
});
|
||||
this.router.get('/schools/:school/users/:id/groups', this.auth, (req, res, next) => {
|
||||
this.database.getUserGroups(req.params.school, req.params.id)
|
||||
.then((data) => {
|
||||
res.json(data);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
// Groups
|
||||
this.router.get('/schools/:school/groups/', this.auth, (req, res, next) => {
|
||||
this.database.getGroups(req.params.school)
|
||||
.then((data) => {
|
||||
res.json(data);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
this.router.post('/schools/:school/groups/', this.auth, (req, res, next) => {
|
||||
this.database.createGroup(req.params.school, req.body)
|
||||
.then((data) => {
|
||||
res.json(data);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
this.router.use('/*', (req, res, next) => {
|
||||
next(new NotFoundError());
|
||||
|
@ -116,12 +139,6 @@ export default class API {
|
|||
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) {
|
||||
|
@ -135,7 +152,7 @@ export default class API {
|
|||
try {
|
||||
return jwt.verify(token, jwkToPem(key), {
|
||||
algorithms: ['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512'],
|
||||
ignoreExpiration: true,
|
||||
//ignoreExpiration: true,
|
||||
});
|
||||
} catch (e) {
|
||||
return a;
|
||||
|
@ -147,11 +164,11 @@ export default class API {
|
|||
return verified;
|
||||
};
|
||||
if (options.type === 'PWD') { // not used
|
||||
return this.database.getUserByEmail(options.email)
|
||||
return this.database.getUserByEmail(school, options.email)
|
||||
.then(data => checkLoginPassword(data.pwd_hash, options.pwd) && data);
|
||||
} else if (options.type === 'OID') {
|
||||
} else if (options.type === 'OID') { // TODO: create user if user not found? no.
|
||||
return checkLoginToken(school, options.id_token)
|
||||
.then(data => this.database.getUserByEmail(data.upn));
|
||||
.then(data => this.database.getUserByEmail(school, data.upn));
|
||||
}
|
||||
return new BadRequestError();
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import mysql from 'mysql';
|
|||
import semver from 'semver';
|
||||
|
||||
import { fatal, getVersion } from './utils';
|
||||
import { NotFoundError } from './errors';
|
||||
import { NotFoundError, attachNoun } from './errors';
|
||||
|
||||
const DATABASE = 'chronos';
|
||||
|
||||
|
@ -39,7 +39,7 @@ export default class Database {
|
|||
domain: undefined,
|
||||
})) : [],
|
||||
}))
|
||||
.catch(err => Promise.reject(err.withNoun('School')));
|
||||
.catch(attachNoun('School'));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
|
@ -50,17 +50,7 @@ export default class Database {
|
|||
WHERE user.school = ?
|
||||
`, [school]);
|
||||
}
|
||||
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) {
|
||||
async getUser(school, id) {
|
||||
return this.query(`
|
||||
SELECT *
|
||||
FROM user
|
||||
|
@ -69,18 +59,56 @@ export default class Database {
|
|||
`, [school, id], {
|
||||
single: true,
|
||||
})
|
||||
.catch(err => Promise.reject(err.withNoun('User')));
|
||||
.catch(attachNoun('User'));
|
||||
}
|
||||
async getUserByEmail(email) {
|
||||
async getUserByEmail(school, email) {
|
||||
// assumes unique email
|
||||
return this.query(`
|
||||
SELECT *
|
||||
FROM user
|
||||
WHERE user.email = ?
|
||||
`, [email], {
|
||||
WHERE user.school = ?
|
||||
AND user.email = ?
|
||||
`, [school, email], {
|
||||
single: true,
|
||||
})
|
||||
.catch(err => Promise.reject(err.withNoun('User')));
|
||||
.catch(attachNoun('User'));
|
||||
}
|
||||
async getUserGroups(school, id) {
|
||||
return this.query(`
|
||||
SELECT *
|
||||
FROM member, group_
|
||||
WHERE member.group_ = group_.id
|
||||
AND member.user = ?
|
||||
`, [id])
|
||||
.catch(attachNoun('Group'));
|
||||
}
|
||||
|
||||
async getGroups(school) {
|
||||
return this.query(`
|
||||
SELECT *
|
||||
FROM user, member, group_
|
||||
WHERE member.group_ = group_.id
|
||||
AND member.user = user.id
|
||||
AND user.school = ?
|
||||
`, [school]);
|
||||
}
|
||||
async createGroup(school, data) {
|
||||
console.log(data);
|
||||
const group = await this.query(`
|
||||
INSERT INTO group_ (name, type)
|
||||
VALUES (?, ?)
|
||||
`, [data.name, data.type]);
|
||||
if (data.type === 'MEN') {
|
||||
await this.query(`
|
||||
INSERT INTO group_mentor (id, level, year)
|
||||
VALUES (?, ?, ?)
|
||||
`, [group.insertId, data.mentor_level, data.mentor_year]);
|
||||
}
|
||||
const insertMember = u => this.query(`
|
||||
INSERT INTO member (user, group_)
|
||||
VALUES (?, ?)
|
||||
`, [u, group.insertId]);
|
||||
await Promise.all(data.members.map(insertMember));
|
||||
}
|
||||
|
||||
query(query, values, options = {}) {
|
||||
|
|
|
@ -59,3 +59,12 @@ export class BadRequestError extends WebError {
|
|||
this.code = 400;
|
||||
}
|
||||
}
|
||||
|
||||
export function attachNoun(noun) {
|
||||
return (err) => {
|
||||
if (err.withNoun) {
|
||||
return Promise.reject(err.withNoun(noun));
|
||||
}
|
||||
return Promise.reject(err);
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue