diff --git a/app/models/api/FileGroup.model.js b/app/models/api/FileGroup.model.js index 36d5182..9a2672f 100644 --- a/app/models/api/FileGroup.model.js +++ b/app/models/api/FileGroup.model.js @@ -18,10 +18,6 @@ class FileGroup extends VersionedModel { } } - accessible_by(user, mode = 'view') { - return user.can(`files:${this.UUID}:${mode}`) - } - // Static and instance methods can go here get page() { const Page = require('./Page.model') diff --git a/app/models/api/Page.model.js b/app/models/api/Page.model.js index 585d3ba..f9a2f7c 100644 --- a/app/models/api/Page.model.js +++ b/app/models/api/Page.model.js @@ -170,20 +170,17 @@ class Page extends VersionedModel { } // ================= SECURITY ================= - accessible_by(user, mode = 'view') { - const base_access = user.can(`page:${this.UUID}:${mode}`) - } async is_accessible_by(user, mode = 'view') { - const can_manage = user.can(`page:${this.UUID}:manage`) - const can_update = user.can(`page:${this.UUID}:update`) - const can_view = user.can(`page:${this.UUID}:view`) - const can_all = user.can(`page:${this.UUID}`) + const can_manage = await user.can(`page:${this.UUID}:manage`) + const can_update = await user.can(`page:${this.UUID}:update`) + const can_view = await user.can(`page:${this.UUID}:view`) + const can_all = await user.can(`page:${this.UUID}`) // Allow universal access if ( can_all ) return true // deny if blocked - else if ( user.can(`page:${this.UUID}:block`) ) return false + else if ( await user.can(`page:${this.UUID}:block`) ) return false // manage, update, view can view else if ( mode === 'view' && (can_manage || can_update || can_view) ) return true // manage, update can update @@ -236,9 +233,9 @@ class Page extends VersionedModel { async unshare_with(user) { // Remove this page from the user's permissions - if ( user.can(`page:${this.UUID}`) ) user.disallow(`page:${this.UUID}`) + if ( await user.can(`page:${this.UUID}`) ) user.disallow(`page:${this.UUID}`) for ( const level of ['view', 'update', 'manage'] ) { - if ( user.can(`page:${this.UUID}:${level}`) ) user.disallow(`page:${this.UUID}:${level}`) + if ( await user.can(`page:${this.UUID}:${level}`) ) user.disallow(`page:${this.UUID}:${level}`) } // Remove the user from this page's access lists diff --git a/app/models/auth/PublicUser.model.js b/app/models/auth/PublicUser.model.js new file mode 100644 index 0000000..6bb45e7 --- /dev/null +++ b/app/models/auth/PublicUser.model.js @@ -0,0 +1,99 @@ +const User = require('./User.model') +const uuid = require('uuid/v4') + +class PublicUserModel extends User { + static get services() { + return [...super.services, 'models'] + } + + static get schema() { + return { + ...super.schema, + RequestData: { + hostname: String, + ips: [String], + ip: String, + date: String, + requests: [{ + method: String, + original_url: String, + xhr: Boolean, + user_id: String, + date: { type: Date, default: () => new Date }, + }], + }, + } + } + + static today() { + const date = new Date() + return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}` + } + + static async get_for_request(request) { + let user = await this.findOne({ + 'RequestData.hostname': request.hostname, + 'RequestData.ip': request.headers['x-forwarded-for'] || request.connection.remoteAddress, + 'RequestData.date': this.today(), + }) + + if ( !user ) user = await this.create_for_request(request) + + await user.record_request(request) + await user.save() + return user + } + + static async create_for_request(request) { + const data = { + hostname: request.hostname, + ips: request.ips, + ip: request.headers['x-forwarded-for'] || request.connection.remoteAddress, + date: this.today(), + requests: [], + } + + const user = new this({ + uid: `public-user-${uuid()}`, + RequestData: data, + }) + await user.save() + return user + } + + async record_request(request) { + if ( !this.RequestData.requests ) { + this.RequestData.requests = []; + } + + this.RequestData.requests.push({ + method: request.method, + original_url: request.originalUrl, + xhr: request.xhr, + user_id: request.user ? request.user.id : '', + }) + } + + async get_root_page() { + const Page = this.models.get('api:Page') + let page = await Page.findOne({OrgUserId: this._id, ParentId: '0'}) + + if ( !page ) { + page = new Page({ + Name: 'Public Root Page', + OrgUserId: this._id, + ParentId: '0', + NodeIds: [], + ChildPageIds: [], + noDelete: true, + virtual: true, + }) + + await page.save() + } + + return page + } +} + +module.exports = exports = PublicUserModel diff --git a/app/models/auth/PublicUserPermission.model.js b/app/models/auth/PublicUserPermission.model.js new file mode 100644 index 0000000..751c682 --- /dev/null +++ b/app/models/auth/PublicUserPermission.model.js @@ -0,0 +1,26 @@ +const { Model } = require('flitter-orm') +const uuid = require('uuid/v4') + +class PublicUserPermissionModel extends Model { + static get schema() { + return { + associated_user_id: String, + permission: String, + grant_date: { type: Date, default: () => new Date }, + UUID: { type: String, default: uuid }, + } + } + + static async can(permission) { + const permission_parts = permission.split(':'); + const match = await this.findOne({ + permission: { + $in: permission_parts + } + }) + + return !!match + } +} + +module.exports = exports = PublicUserPermissionModel diff --git a/app/routing/middleware/auth/ApiRoute.middleware.js b/app/routing/middleware/auth/ApiRoute.middleware.js new file mode 100644 index 0000000..e93dec2 --- /dev/null +++ b/app/routing/middleware/auth/ApiRoute.middleware.js @@ -0,0 +1,25 @@ +const { Middleware } = require('libflitter') + +class ApiRoute extends Middleware { + static get services() { + return [...super.services, 'models'] + } + + async test(req, res, next, { allow_public = false }) { + console.log({allow_public}) + // If we have an authenticated session, just continue + if ( req.is_auth ) { + return next() + } else if ( allow_public ) { + const PublicUser = this.models.get('auth:PublicUser') + req.user = await PublicUser.get_for_request(req) + return next() + } else { + // If not signed in, save the target url so we can redirect back here after auth + req.session.auth.flow = req.originalUrl + return res.redirect('/auth/login') + } + } +} + +module.exports = ApiRoute diff --git a/app/routing/routers/api/v1.routes.js b/app/routing/routers/api/v1.routes.js deleted file mode 100644 index 6545333..0000000 --- a/app/routing/routers/api/v1.routes.js +++ /dev/null @@ -1,116 +0,0 @@ -/* - * API v1 Routes - * ------------------------------------------------------------- - * Description here - */ -const index = { - - prefix: '/api/v1', - - middleware: [ - 'auth:UserOnly', - ], - - get: { - '/token': [ - 'controller::api:v1:Misc.get_token', - ], - - // Get the file ref node config for the specified file ref - '/files/:PageId/:NodeId/get/:FilesId': ['controller::api:v1:File.get_config'], - - // Download the specified file ID from the specified file ref node - '/files/:PageId/:NodeId/get/:FilesId/:FileId': ['controller::api:v1:File.download'], - - // Get the data for the specified page - '/page/:PageId': ['controller::api:v1:Page.get_page'], - - // Get the available versions of the given page - '/page/:PageId/versions': ['controller::api:v1:Page.get_page_versions'], - - // Get the nodes present on the specified page - '/page/:PageId/nodes': ['controller::api:v1:Page.get_nodes'], - - // Get the user's menu tree - '/menu/items': ['controller::api:v1:Menu.get_items'], - - // Get the database ref node config for the specified database - '/db/:PageId/:NodeId/get/:DatabaseId': ['controller::api:v1:FormDatabase.get_config'], - - // Get the column config records for the specified database - '/db/:PageId/:NodeId/get/:DatabaseId/columns': [ 'controller::api:v1:FormDatabase.get_columns' ], - - // Get the row records for the specified database - '/db/:PageId/:NodeId/get/:DatabaseId/data': [ 'controller::api:v1:FormDatabase.get_data' ], - - // Get the code ref node config for the specified code editor - '/code/:PageId/:NodeId/get/:CodiumId': ['controller::api:v1:FormCode.get_config'], - - // Export the entire personal tree as HTML - '/data/export/html': ['controller::Export.html_export'], - - '/search': ['controller::api:v1:Misc.get_search'], - - '/offline/prefetch': ['controller::api:v1:Offline.do_prefetch'], - }, - - post: { - // Upload the file in the 'uploaded_file' key to the specified file ref node - '/file/upload/:PageId/:NodeId/:FilesId': ['middleware::upload:UploadFile', 'controller::api:v1:File.save_upload'], - - // Create a new file ref node - '/files/:PageId/:NodeId/create': ['controller::api:v1:File.create_config'], - - // Delete a file ref node and its files - '/files/:PageId/:NodeId/delete/:FilesId': ['controller::api:v1:File.delete_group'], - - // Save the data for the specified page - '/page/:PageId/save': ['controller::api:v1:Page.save_page'], - - // Revert the page to a previous version - '/page/:PageId/versions/revert': ['controller::api:v1:Page.revert_version'], - - // Save the node data for the specified page - '/page/:PageId/nodes/save': ['controller::api:v1:Page.save_nodes'], - - '/page/:PageId/nodes/save_one': ['controller::api:v1:Page.save_node_to_page'], - - // Create a new page in the personal root - '/page/create': ['controller::api:v1:Page.create_top_level'], - - // Create a new page as a child of the specified page - '/page/create-child': ['controller::api:v1:Page.create_child'], - - // Delete the specified page - '/page/delete/:PageId': ['controller::api:v1:Page.delete_page'], - - // Create a new database ref config - '/db/:PageId/:NodeId/create': ['controller::api:v1:FormDatabase.create_new'], - - // Set the column configs for a database ref - '/db/:PageId/:NodeId/set/:DatabaseId/columns': [ 'controller::api:v1:FormDatabase.set_columns' ], - - // Set the database name - '/db/:PageId/:NodeId/set/:DatabaseId/Name': [ 'controller::api:v1:FormDatabase.set_name' ], - - // Delete the specified database ref - '/db/:PageId/:NodeId/drop/:DatabaseId': [ 'controller::api:v1:FormDatabase.drop_database' ], - - // Set the row data for the specified database ref - '/db/:PageId/:NodeId/set/:DatabaseId/data': ['controller::api:v1:FormDatabase.set_data'], - - // Create a new code ref config - '/code/:PageId/:NodeId/create': ['controller::api:v1:FormCode.create_new'], - - // Set the data for the specified code ref - '/code/:PageId/:NodeId/set/:CodiumId': ['controller::api:v1:FormCode.set_values'], - - // delete the specified code ref - '/code/:PageId/:NodeId/delete/:CodiumId': ['controller::api:v1:FormCode.drop_code'], - - // re-sync data when an offline client goes back online - '/offline/sync': ['controller::api:v1:Offline.do_sync'], - }, -} - -module.exports = exports = index diff --git a/app/routing/routers/api/v1/code.routes.js b/app/routing/routers/api/v1/code.routes.js new file mode 100644 index 0000000..027ec45 --- /dev/null +++ b/app/routing/routers/api/v1/code.routes.js @@ -0,0 +1,24 @@ +module.exports = exports = { + + prefix: '/api/v1/code', + + middleware: [ + 'auth:ApiRoute', + ], + + get: { + // Get the code ref node config for the specified code editor + '/:PageId/:NodeId/get/:CodiumId': ['controller::api:v1:FormCode.get_config'], + }, + + post: { + // Create a new code ref config + '/:PageId/:NodeId/create': ['controller::api:v1:FormCode.create_new'], + + // Set the data for the specified code ref + '/:PageId/:NodeId/set/:CodiumId': ['controller::api:v1:FormCode.set_values'], + + // delete the specified code ref + '/:PageId/:NodeId/delete/:CodiumId': ['controller::api:v1:FormCode.drop_code'], + }, +} diff --git a/app/routing/routers/api/v1/data.routes.js b/app/routing/routers/api/v1/data.routes.js new file mode 100644 index 0000000..b162b54 --- /dev/null +++ b/app/routing/routers/api/v1/data.routes.js @@ -0,0 +1,17 @@ +module.exports = exports = { + + prefix: '/api/v1/data', + + middleware: [ + 'auth:ApiRoute' + ], + + get: { + // Export the entire personal tree as HTML + '/export/html': ['controller::Export.html_export'], + }, + + post: { + + }, +} diff --git a/app/routing/routers/api/v1/db.routes.js b/app/routing/routers/api/v1/db.routes.js new file mode 100644 index 0000000..1fa3c9c --- /dev/null +++ b/app/routing/routers/api/v1/db.routes.js @@ -0,0 +1,36 @@ +module.exports = exports = { + + prefix: '/api/v1/db', + + middleware: [ + 'auth:ApiRoute' + ], + + get: { + // Get the database ref node config for the specified database + '/:PageId/:NodeId/get/:DatabaseId': ['controller::api:v1:FormDatabase.get_config'], + + // Get the column config records for the specified database + '/:PageId/:NodeId/get/:DatabaseId/columns': [ 'controller::api:v1:FormDatabase.get_columns' ], + + // Get the row records for the specified database + '/:PageId/:NodeId/get/:DatabaseId/data': [ 'controller::api:v1:FormDatabase.get_data' ], + }, + + post: { + // Create a new database ref config + '/:PageId/:NodeId/create': ['controller::api:v1:FormDatabase.create_new'], + + // Set the column configs for a database ref + '/:PageId/:NodeId/set/:DatabaseId/columns': [ 'controller::api:v1:FormDatabase.set_columns' ], + + // Set the database name + '/:PageId/:NodeId/set/:DatabaseId/Name': [ 'controller::api:v1:FormDatabase.set_name' ], + + // Delete the specified database ref + '/:PageId/:NodeId/drop/:DatabaseId': [ 'controller::api:v1:FormDatabase.drop_database' ], + + // Set the row data for the specified database ref + '/:PageId/:NodeId/set/:DatabaseId/data': ['controller::api:v1:FormDatabase.set_data'], + }, +} diff --git a/app/routing/routers/api/v1/files.routes.js b/app/routing/routers/api/v1/files.routes.js new file mode 100644 index 0000000..5e7b3f6 --- /dev/null +++ b/app/routing/routers/api/v1/files.routes.js @@ -0,0 +1,28 @@ +module.exports = exports = { + + prefix: '/api/v1/files', + + middleware: [ + 'auth:ApiRoute', + ], + + get: { + // Get the file ref node config for the specified file ref + '/:PageId/:NodeId/get/:FilesId': ['controller::api:v1:File.get_config'], + + // Download the specified file ID from the specified file ref node + '/:PageId/:NodeId/get/:FilesId/:FileId': ['controller::api:v1:File.download'], + }, + + post: { + // FIXME - files, not file. Fix in front-end! + // Upload the file in the 'uploaded_file' key to the specified file ref node + '/file/upload/:PageId/:NodeId/:FilesId': ['middleware::upload:UploadFile', 'controller::api:v1:File.save_upload'], + + // Create a new file ref node + '/:PageId/:NodeId/create': ['controller::api:v1:File.create_config'], + + // Delete a file ref node and its files + '/:PageId/:NodeId/delete/:FilesId': ['controller::api:v1:File.delete_group'], + }, +} diff --git a/app/routing/routers/api/v1/menu.routes.js b/app/routing/routers/api/v1/menu.routes.js new file mode 100644 index 0000000..8669feb --- /dev/null +++ b/app/routing/routers/api/v1/menu.routes.js @@ -0,0 +1,17 @@ +module.exports = exports = { + + prefix: '/api/v1/menu', + + middleware: [ + 'auth:ApiRoute', + ], + + get: { + // Get the user's menu tree + '/items': ['controller::api:v1:Menu.get_items'], + }, + + post: { + + }, +} diff --git a/app/routing/routers/api/v1/offline.routes.js b/app/routing/routers/api/v1/offline.routes.js new file mode 100644 index 0000000..5ead098 --- /dev/null +++ b/app/routing/routers/api/v1/offline.routes.js @@ -0,0 +1,17 @@ +module.exports = exports = { + + prefix: '/api/v1/offline', + + middleware: [ + 'auth:ApiRoute', + ], + + get: { + '/prefetch': ['controller::api:v1:Offline.do_prefetch'], + }, + + post: { + // re-sync data when an offline client goes back online + '/sync': ['controller::api:v1:Offline.do_sync'], + }, +} diff --git a/app/routing/routers/api/v1/page.routes.js b/app/routing/routers/api/v1/page.routes.js new file mode 100644 index 0000000..6cba76a --- /dev/null +++ b/app/routing/routers/api/v1/page.routes.js @@ -0,0 +1,40 @@ +module.exports = exports = { + prefix: '/api/v1/page', + + middleware: [ + 'auth:ApiRoute', + ], + + get: { + // Get the data for the specified page + '/:PageId': ['controller::api:v1:Page.get_page'], + + // Get the available versions of the given page + '/:PageId/versions': ['controller::api:v1:Page.get_page_versions'], + + // Get the nodes present on the specified page + '/:PageId/nodes': ['controller::api:v1:Page.get_nodes'], + }, + + post: { + // Save the data for the specified page + '/:PageId/save': ['controller::api:v1:Page.save_page'], + + // Revert the page to a previous version + '/:PageId/versions/revert': ['controller::api:v1:Page.revert_version'], + + // Save the node data for the specified page + '/:PageId/nodes/save': ['controller::api:v1:Page.save_nodes'], + + '/:PageId/nodes/save_one': ['controller::api:v1:Page.save_node_to_page'], + + // Create a new page in the personal root + '/create': ['controller::api:v1:Page.create_top_level'], + + // Create a new page as a child of the specified page + '/create-child': ['controller::api:v1:Page.create_child'], + + // Delete the specified page + '/delete/:PageId': ['controller::api:v1:Page.delete_page'], + }, +} diff --git a/app/routing/routers/api/v1/search.routes.js b/app/routing/routers/api/v1/search.routes.js new file mode 100644 index 0000000..d026e31 --- /dev/null +++ b/app/routing/routers/api/v1/search.routes.js @@ -0,0 +1,16 @@ +module.exports = exports = { + + prefix: '/api/v1/search', + + middleware: [ + 'auth:ApiRoute', + ], + + get: { + '/': ['controller::api:v1:Misc.get_search'], + }, + + post: { + + }, +} diff --git a/app/routing/routers/api/v1/token.routes.js b/app/routing/routers/api/v1/token.routes.js new file mode 100644 index 0000000..a2f537a --- /dev/null +++ b/app/routing/routers/api/v1/token.routes.js @@ -0,0 +1,18 @@ +module.exports = exports = { + + prefix: '/api/v1/token', + + middleware: [ + 'auth:ApiRoute', + ], + + get: { + '/': [ + 'controller::api:v1:Misc.get_token', + ], + }, + + post: { + + }, +} diff --git a/app/routing/routers/index.routes.js b/app/routing/routers/index.routes.js index 34928f6..5874a5f 100644 --- a/app/routing/routers/index.routes.js +++ b/app/routing/routers/index.routes.js @@ -41,16 +41,13 @@ const index = { // e.g. controller::Home.welcome '/': ['controller::Home.welcome'], - '/stat': ['controller::Home.get_stat'], + '/stat': [['middleware::auth:ApiRoute', {allow_public: true}], 'controller::Home.get_stat'], // Placeholder for auth dashboard. You'd replace this with // your own route protected by 'middleware::auth:UserOnly' '/dash': ['middleware::auth:UserOnly', 'controller::Home.toApp'], '/start': ['middleware::auth:UserOnly', 'controller::Home.toApp'], '/login': ['middleware::auth:GuestOnly', 'controller::Home.get_login'], - '/test-json': ['controller::Export.json_export'], - '/test-markdown': ['controller::Export.markdown_export'], - '/test-html': ['controller::Export.html_export'] }, /*