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/',
src: ['dist/**'],
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>
<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="bundle.js"></script>
</body>

View File

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

View File

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

View File

@ -1,20 +1,78 @@
import Router from 'express';
import { WebError, UnauthenticatedError } from './errors';
export default class API {
constructor() {
constructor(database) {
this.database = database;
// Binds
this.auth = this.auth.bind(this);
this.router = Router({
strict: true,
});
// Routes
this.router.get('/', (req, res) => {
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) {
res.end('not implemented');
auth(req, res, next) {
// 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();
}
}

View File

@ -1,6 +1,7 @@
import mysql from 'mysql';
import semver from 'semver';
import { fatal, getVersion } from './utils';
import { NotFoundError } from './errors';
const DATABASE = 'chronos';
@ -12,12 +13,63 @@ export default class Database {
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, ''));
return new Promise((resolve, reject) => {
this.connection.query(query, values, (err, results, fields) => {
if (err) {
reject(err);
} else if (options.required === true && results.length < 1) {
reject(new NotFoundError());
}
resolve(results, fields);
});
@ -40,7 +92,7 @@ export default class Database {
} else if (semver.satisfies(oldVersion, '~0')) { // lmao forces database migration
// database needs to be updated
return this.query(`DROP DATABASE ${DATABASE}`)
.then(() => this.checkAndMigrate());
.then(() => this.checkAndMigrate());
}
// database does not exist
@ -100,7 +152,7 @@ export default class Database {
`CREATE TABLE group_mentor (
id INT NOT NULL,
level TINYINT NOT NULL,
year YEAR NOT NULL,
year YEAR(4) NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY (id) REFERENCES group_(id) ON DELETE CASCADE ON UPDATE CASCADE
)`,
@ -153,12 +205,21 @@ export default class Database {
VALUES ('version', '${getVersion()}')
`);
// TODO: Build admin interface to add schools
// add first school
await this.query(`
const firstSchool = await this.query(`
INSERT INTO school (name, domain)
VALUES (?, ?)
`, ['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;
}
}

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;
}
}