1
0
Fork 0

Write user endpoint

master
Ambrose Chua 2017-03-23 23:47:27 +08:00
parent 826e5d99bf
commit 779815d2db
9 changed files with 209 additions and 10 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
# Common files
.DS_Store
node_modules/
npm-debug.log
# Build products
dist/
# Temporary files
oid_settings.json
nginx.conf

View File

@ -47,6 +47,12 @@ module.exports = function (grunt) {
cwd: './node_modules/bootstrap/', cwd: './node_modules/bootstrap/',
src: ['dist/**'], src: ['dist/**'],
dest: './dist/app/assets/bootstrap/' dest: './dist/app/assets/bootstrap/'
},
{
expand: true,
cwd: './node_modules/oidc-client/',
src: ['dist/**'],
dest: './dist/app/assets/oidc-client/'
} }
] ]
} }

27
app/components/input.jsx Normal file
View File

@ -0,0 +1,27 @@
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: '',
};

View File

@ -7,6 +7,7 @@
<div id="root"> <div id="root">
</div> </div>
<script src="bundle.js"></script> <script src="assets/oidc-client/dist/oidc-client.min.js"></script>
<script src="assets/bootstrap/dist/js/bootstrap.min.js"></script> <script src="assets/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="bundle.js"></script>
</body> </body>

View File

@ -9,7 +9,7 @@ export default class PageLogin extends React.Component {
return ( return (
<Container> <Container>
<Button> <Button>
Login Login with Office365
</Button> </Button>
</Container> </Container>
); );

View File

@ -37,6 +37,7 @@
"bootstrap": "^4.0.0-alpha.6", "bootstrap": "^4.0.0-alpha.6",
"express": "^4.14.1", "express": "^4.14.1",
"mysql": "^2.13.0", "mysql": "^2.13.0",
"oidc-client": "^1.3.0-beta.3",
"react": "^15.4.2", "react": "^15.4.2",
"react-dom": "^15.4.2", "react-dom": "^15.4.2",
"react-router-dom": "^4.0.0-beta.6", "react-router-dom": "^4.0.0-beta.6",

View File

@ -1,20 +1,78 @@
import Router from 'express'; import Router from 'express';
import { WebError, UnauthenticatedError } from './errors';
export default class API { export default class API {
constructor() { constructor(database) {
this.database = database;
// Binds
this.auth = this.auth.bind(this);
this.router = Router({ this.router = Router({
strict: true, strict: true,
}); });
// Routes
this.router.get('/', (req, res) => { this.router.get('/', (req, res) => {
res.end('API v1'); res.end('API v1');
}); });
this.router.all('/*', this.constructor.auth); // Schools
this.router.get('/schools/', (req, res, next) => {
this.database.getSchools()
.then((data) => {
res.json(data);
}).catch(next);
});
this.router.get('/schools/:school', (req, res, next) => {
this.database.getSchool(req.params.school)
.then((data) => {
res.json(data);
}).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);
});
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);
}).catch(next);
});
// this.router.all('/*', this.auth);
this.router.use(API.error);
} }
static auth(req, res, next) { auth(req, res, next) {
res.end('not implemented'); // res.end('not implemented');
if (this.validate(req.get('FakeAuth'))) {
return next();
}
return next(new UnauthenticatedError());
}
// eslint-disable-next-line class-methods-use-this
validate(token) {
return !!token;
}
// 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',
});
}
next(); next();
} }
} }

View File

@ -1,6 +1,7 @@
import mysql from 'mysql'; import mysql from 'mysql';
import semver from 'semver'; import semver from 'semver';
import { fatal, getVersion } from './utils'; import { fatal, getVersion } from './utils';
import { NotFoundError } from './errors';
const DATABASE = 'chronos'; const DATABASE = 'chronos';
@ -12,12 +13,63 @@ export default class Database {
this.checkAndMigrate(); this.checkAndMigrate();
} }
query(query, values) { async getSchools() {
return this.query(`
SELECT id, name, domain
FROM school
`);
}
async getSchool(id) {
return this.query(`
SELECT *
FROM auth RIGHT JOIN school
ON auth.school = school.id
WHERE school.${isNaN(parseInt(id, 10)) ? 'domain' : 'id'} = ?
`, [id], {
required: true,
})
.then(results => ({
id: results[0].id,
name: results[0].name,
domain: results[0].domain,
auth: results.map(r => Object.assign(r, {
school: undefined,
name: undefined,
domain: undefined,
oid_csecret: undefined,
})),
}))
.catch(err => err.withNoun('School'));
}
// eslint-disable-next-line class-methods-use-this
async getUsers(school) {
return this.query(`
SELECT id, name
FROM user
WHERE user.school = ?
`, [school]);
}
async getUser(school, id) {
return this.query(`
SELECT id, name, email, role
FROM user
WHERE user.school = ?
AND user.id = ?
`, [school, id], {
required: true,
})
.catch(err => err.withNoun('User'));
}
query(query, values, options = {}) {
console.log('QUERY:', query.replace(/[\n\t]+/g, ' ').replace(/^ /g, '').replace(/ $/g, '')); console.log('QUERY:', query.replace(/[\n\t]+/g, ' ').replace(/^ /g, '').replace(/ $/g, ''));
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
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) {
reject(new NotFoundError());
} }
resolve(results, fields); resolve(results, fields);
}); });
@ -40,7 +92,7 @@ export default class Database {
} else if (semver.satisfies(oldVersion, '~0')) { // lmao forces database migration } else if (semver.satisfies(oldVersion, '~0')) { // lmao forces database migration
// 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
@ -100,7 +152,7 @@ export default class Database {
`CREATE TABLE group_mentor ( `CREATE TABLE group_mentor (
id INT NOT NULL, id INT NOT NULL,
level TINYINT NOT NULL, level TINYINT NOT NULL,
year YEAR NOT NULL, year YEAR(4) NOT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
FOREIGN KEY (id) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE FOREIGN KEY (id) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE
)`, )`,
@ -153,12 +205,21 @@ export default class Database {
VALUES ('version', '${getVersion()}') VALUES ('version', '${getVersion()}')
`); `);
// TODO: Build admin interface to add schools
// add first school // add first school
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']);
// eslint-disable-next-line global-require
const fs = require('fs');
const tmpsettings = JSON.parse(fs.readFileSync('oid_settings.json', 'utf8'));
await this.query(`
INSERT INTO auth (school, type, oid_meta, oid_cid, oid_csecret)
VALUES (?, ?, ?, ?, ?)
`, [firstSchool.insertId, 'OID', tmpsettings.oid_meta, tmpsettings.oid_cid, tmpsettings.oid_csecret]);
return true; return true;
} }
} }

34
server/errors.js Normal file
View File

@ -0,0 +1,34 @@
export class WebError extends Error {
responseTo(res) {
return res.status(this.code).json(this.toJSON());
}
toJSON() {
return {
type: this.name,
code: this.code,
error: this.message,
};
}
}
export class NotFoundError extends WebError {
constructor(noun = 'Resource') {
super();
this.name = this.constructor.name;
this.message = `${noun} not found`;
this.code = 404;
}
withNoun(noun) {
this.message = `${noun} not found`;
return this;
}
}
export class UnauthenticatedError extends WebError {
constructor() {
super();
this.name = this.constructor.name;
this.message = 'User not authenticated';
this.code = 403;
}
}