Write user endpoint
parent
826e5d99bf
commit
779815d2db
|
@ -0,0 +1,11 @@
|
|||
# Common files
|
||||
.DS_Store
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
|
||||
# Build products
|
||||
dist/
|
||||
|
||||
# Temporary files
|
||||
oid_settings.json
|
||||
nginx.conf
|
|
@ -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/'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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: '',
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -9,7 +9,7 @@ export default class PageLogin extends React.Component {
|
|||
return (
|
||||
<Container>
|
||||
<Button>
|
||||
Login
|
||||
Login with Office365
|
||||
</Button>
|
||||
</Container>
|
||||
);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue