parent
fc5fc14b3f
commit
dfcaf046c6
@ -0,0 +1,121 @@
|
|||||||
|
const Controller = require('libflitter/controller/Controller')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* DatabaseAPI Controller
|
||||||
|
* -------------------------------------------------------------
|
||||||
|
* Put some description here!
|
||||||
|
*/
|
||||||
|
class DatabaseAPI extends Controller {
|
||||||
|
static get services() {
|
||||||
|
return [...super.services, 'models', 'utility']
|
||||||
|
}
|
||||||
|
|
||||||
|
async databases(req, res, next) {
|
||||||
|
const Database = this.models.get('api:db:Database')
|
||||||
|
const dbs = await Database.visible_by_user(req.user)
|
||||||
|
return res.api(dbs.map(x => x.to_api_object()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_database(req, res, next) {
|
||||||
|
return res.api(req.form.database.to_api_object())
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_columns(req, res, next) {
|
||||||
|
const db = req.form.database
|
||||||
|
const cols = (await db.get_columns()).map(x => x.to_api_object())
|
||||||
|
return res.api(cols)
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_columns_order(req, res, next) {
|
||||||
|
return res.api(req.form.database.ColumnIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_data(req, res, next) {
|
||||||
|
const DBEntry = this.models.get('api:db:DBEntry')
|
||||||
|
const db = req.form.database
|
||||||
|
const cursor = await DBEntry.cursor({DatabaseId: db.UUID})
|
||||||
|
|
||||||
|
if ( req.query.sort ) {
|
||||||
|
if ( this.utility.is_json(req.query.sort) ) {
|
||||||
|
const sort = JSON.parse(req.query.sort)
|
||||||
|
if ( typeof sort !== 'object' ) return res.status(400).message('Invalid sort field. Should be JSON object.').api()
|
||||||
|
const sort_obj = {}
|
||||||
|
for ( const field in sort ) {
|
||||||
|
if ( !sort.hasOwnProperty(field) ) continue
|
||||||
|
sort_obj[`RowData.${field}`] = (Number(sort[field]) < 0 ? -1 : 1)
|
||||||
|
}
|
||||||
|
cursor.sort(sort_obj)
|
||||||
|
} else if ( req.query.sort ) {
|
||||||
|
const sort_obj = {}
|
||||||
|
sort_obj[`RowData.${req.query.sort}`] = (req.query.reverse ? -1 : 1)
|
||||||
|
cursor.sort(sort_obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( req.query.limit ) {
|
||||||
|
const limit = Number(req.query.limit)
|
||||||
|
if ( !isNaN(limit) ) cursor.limit(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( req.query.skip ) {
|
||||||
|
const skip = Number(req.query.skip)
|
||||||
|
if ( !isNaN(skip) ) cursor.skip(skip)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( req.query.where ) {
|
||||||
|
if ( !this.utility.is_json(req.query.where) ) {
|
||||||
|
return res.status(400).message('Invalid where field. Should be JSON object.').api()
|
||||||
|
}
|
||||||
|
|
||||||
|
const wheres = JSON.parse(req.query.where)
|
||||||
|
|
||||||
|
if ( typeof wheres !== 'object' ) {
|
||||||
|
return res.status(400).message('Invalid where field. Should be JSON object.').api()
|
||||||
|
}
|
||||||
|
|
||||||
|
const wheres_object = {}
|
||||||
|
for ( const field in wheres ) {
|
||||||
|
if ( !wheres.hasOwnProperty(field) ) continue
|
||||||
|
const value = wheres[field]
|
||||||
|
if ( typeof value !== 'object' ) {
|
||||||
|
wheres_object[`RowData.${field}`] = value
|
||||||
|
} else {
|
||||||
|
const sub_where = {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await DBEntry.from_cursor(cursor)).map(x => {
|
||||||
|
x = x.to_api_object()
|
||||||
|
if ( req.query.flatten ) {
|
||||||
|
x = {
|
||||||
|
uuid: x.uuid,
|
||||||
|
database_id: x.database_id,
|
||||||
|
...x.data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
})
|
||||||
|
return res.api(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_record(req, res, next) {
|
||||||
|
const DBEntry = this.models.get('api:db:DBEntry')
|
||||||
|
const db = req.form.database
|
||||||
|
const record = await DBEntry.findOne({UUID: req.params.record_id, DatabaseId: db.UUID})
|
||||||
|
if ( record ) {
|
||||||
|
const api_obj = record.to_api_object()
|
||||||
|
if ( req.query.flatten ) {
|
||||||
|
return res.api({
|
||||||
|
uuid: api_obj.uuid,
|
||||||
|
database_id: api_obj.database_id,
|
||||||
|
...api_obj.data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return res.api(api_obj)
|
||||||
|
}
|
||||||
|
else return res.status(404).message('Database record not found with that ID.').api()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = DatabaseAPI
|
@ -0,0 +1,95 @@
|
|||||||
|
const Model = require('flitter-orm/src/model/Model')
|
||||||
|
const { ObjectId } = require('mongodb')
|
||||||
|
const User = require('../auth/User.model')
|
||||||
|
const ValidScope = require('../scopes/Valid.scope')
|
||||||
|
const uuid = require('uuid/v4')
|
||||||
|
const jwt = require('jsonwebtoken')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Token Model
|
||||||
|
* -------------------------------------------------------------
|
||||||
|
* Put some description here!
|
||||||
|
*/
|
||||||
|
class Token extends Model {
|
||||||
|
static #default_grants = ['database']
|
||||||
|
|
||||||
|
static get services() {
|
||||||
|
return [...super.services, 'configs']
|
||||||
|
}
|
||||||
|
|
||||||
|
static get schema() {
|
||||||
|
// Return a flitter-orm schema here.
|
||||||
|
return {
|
||||||
|
user_id: ObjectId,
|
||||||
|
token: String,
|
||||||
|
issued: { type: Date, default: () => new Date },
|
||||||
|
expires: {
|
||||||
|
type: Date,
|
||||||
|
default: () => {
|
||||||
|
const d = new Date
|
||||||
|
d.setDate(d.getDate() + 7)
|
||||||
|
return d
|
||||||
|
},
|
||||||
|
},
|
||||||
|
valid: { type: Boolean, default: true },
|
||||||
|
grants: [String],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static scopes = [new ValidScope]
|
||||||
|
|
||||||
|
static async create(user) {
|
||||||
|
const token = new this({ user_id: user._id, grants: this.#default_grants })
|
||||||
|
await token.save()
|
||||||
|
token.token = token.generate()
|
||||||
|
return await token.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
static async for_user(user) {
|
||||||
|
const token = await this.findOne({ user_id: user._id })
|
||||||
|
return token ? token : await this.create(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
static verify(token) {
|
||||||
|
const secret = this.prototype.configs.get('server.session.secret')
|
||||||
|
const server = this.prototype.configs.get('app.url')
|
||||||
|
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
jwt.verify(token, secret, (err, decoded) => {
|
||||||
|
if ( err ) rej(err)
|
||||||
|
else if ( decoded.iss !== server ) rej(new Error('Invalid token issuer: '+decoded.iss))
|
||||||
|
else {
|
||||||
|
this.findOne({ user_id: ObjectId(decoded.sub) }).then(result => {
|
||||||
|
if ( !result ) rej(new Error('Unable to find associated token. It may have been manually invalidated.'))
|
||||||
|
else res(result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
generate() {
|
||||||
|
const secret = this.configs.get('server.session.secret')
|
||||||
|
const server = this.configs.get('app.url')
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
sub: String(this.user_id),
|
||||||
|
iss: server,
|
||||||
|
permissions: this.grants.join(','),
|
||||||
|
exp: (this.expires.getTime()/1000 | 0),
|
||||||
|
iat: (this.issued.getTime()/1000 | 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
return jwt.sign(payload, secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
user() {
|
||||||
|
return this.has_one(User, 'user_id', '_id')
|
||||||
|
}
|
||||||
|
|
||||||
|
can(grant) {
|
||||||
|
return this.grants.includes(grant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = Token
|
@ -0,0 +1,11 @@
|
|||||||
|
const Scope = require('flitter-orm/src/model/Scope')
|
||||||
|
|
||||||
|
class ValidScope extends Scope {
|
||||||
|
async filter(to_filter) {
|
||||||
|
return to_filter.equal('valid', true)
|
||||||
|
.greater_than('expires', new Date)
|
||||||
|
.less_than('issued', new Date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = ValidScope
|
@ -0,0 +1,30 @@
|
|||||||
|
const Middleware = require('libflitter/middleware/Middleware')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* DatabaseRoute Middleware
|
||||||
|
* -------------------------------------------------------------
|
||||||
|
* Put some description here!
|
||||||
|
*/
|
||||||
|
class DatabaseRoute extends Middleware {
|
||||||
|
static get services() {
|
||||||
|
return [...super.services, 'models']
|
||||||
|
}
|
||||||
|
|
||||||
|
async test(req, res, next, args = {}){
|
||||||
|
const Database = this.models.get('api:db:Database')
|
||||||
|
|
||||||
|
const id = req.params.database_id ? req.params.database_id : (req.query.database_id ? req.query.database_id : false)
|
||||||
|
if ( !id ) return res.status(400).message('Missing required: database_id').api()
|
||||||
|
|
||||||
|
const db = await Database.findOne({UUID: id})
|
||||||
|
if ( !db ) return res.status(404).message('Unable to find database with that ID.').api()
|
||||||
|
if ( !(await db.is_accessible_by(req.user)) ) return req.security.deny()
|
||||||
|
|
||||||
|
if ( !req.form ) req.form = {}
|
||||||
|
req.form.database = db
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = DatabaseRoute
|
@ -0,0 +1,52 @@
|
|||||||
|
const Middleware = require('libflitter/middleware/Middleware')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* BearerToken Middleware
|
||||||
|
* -------------------------------------------------------------
|
||||||
|
* Put some description here!
|
||||||
|
*/
|
||||||
|
class BearerToken extends Middleware {
|
||||||
|
static get services() {
|
||||||
|
return [...super.services, 'models']
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run the middleware test.
|
||||||
|
* This method is required by all Flitter middleware.
|
||||||
|
* It should either call the next function in the stack,
|
||||||
|
* or it should handle the response accordingly.
|
||||||
|
*/
|
||||||
|
async test(req, res, next, args = []){
|
||||||
|
const Token = this.models.get('api:Token')
|
||||||
|
|
||||||
|
const token_string = req.headers.authorization
|
||||||
|
if ( !token_string ) return this.fail(res, )
|
||||||
|
else if ( !token_string.startsWith('bearer ') ) return this.fail(res, 'Invalid authorization token. Prefix with "bearer".')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = await Token.verify(token_string.replace('bearer ', ''))
|
||||||
|
const user = await token.user()
|
||||||
|
if ( !user || !token ) return this.fail(res)
|
||||||
|
|
||||||
|
if ( Array.isArray(args) ) {
|
||||||
|
for (const grant of args) {
|
||||||
|
if (!token.can(grant)) {
|
||||||
|
return this.fail(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = user
|
||||||
|
req.token = token
|
||||||
|
next()
|
||||||
|
} catch (e) {
|
||||||
|
return this.fail(res, String(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fail(res, msg = 'Unauthorized') {
|
||||||
|
return res.status(401).message(msg).api({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = exports = BearerToken
|
@ -0,0 +1,43 @@
|
|||||||
|
module.exports = exports = {
|
||||||
|
|
||||||
|
prefix: '/db_api/v1',
|
||||||
|
|
||||||
|
middleware: [
|
||||||
|
// JWT authorization middleware. Sets req.user and req.token.
|
||||||
|
// Second param is array of required grants.
|
||||||
|
['api:auth:BearerToken', ['database']],
|
||||||
|
],
|
||||||
|
|
||||||
|
get: {
|
||||||
|
'/': [ 'controller::api:v1:DatabaseAPI.databases' ],
|
||||||
|
|
||||||
|
'/:database_id': [
|
||||||
|
'middleware::api:DatabaseRoute',
|
||||||
|
'controller::api:v1:DatabaseAPI.get_database',
|
||||||
|
],
|
||||||
|
|
||||||
|
'/:database_id/columns': [
|
||||||
|
'middleware::api:DatabaseRoute',
|
||||||
|
'controller::api:v1:DatabaseAPI.get_columns',
|
||||||
|
],
|
||||||
|
|
||||||
|
'/:database_id/columns/order': [
|
||||||
|
'middleware::api:DatabaseRoute',
|
||||||
|
'controller::api:v1:DatabaseAPI.get_columns_order',
|
||||||
|
],
|
||||||
|
|
||||||
|
'/:database_id/data': [
|
||||||
|
'middleware::api:DatabaseRoute',
|
||||||
|
'controller::api:v1:DatabaseAPI.get_data',
|
||||||
|
],
|
||||||
|
|
||||||
|
'/:database_id/record/:record_id': [
|
||||||
|
'middleware::api:DatabaseRoute',
|
||||||
|
'controller::api:v1:DatabaseAPI.get_record',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
post: {
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
Loading…
Reference in new issue