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/',
|
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/'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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