const Model = require("flitter-orm/src/model/Model"); const { ObjectId } = require("mongodb"); const uuid = require('uuid/v4'); const ActiveScope = require('../scopes/Active.scope') /* * Page Model * ------------------------------------------------------------- * Put some description here! */ class Page extends Model { static get services() { return [...super.services, "models"]; } static get schema() { // Return a flitter-orm schema here. return { UUID: {type: String, default: () => uuid()}, Name: String, OrgUserId: ObjectId, IsPublic: {type: Boolean, default: true}, IsVisibleInMenu: {type: Boolean, default: true}, ParentId: String, NodeIds: [String], CreatedAt: {type: Date, default: () => new Date}, UpdatedAt: {type: Date, default: () => new Date}, DeletedAt: Date, Active: {type: Boolean, default: true}, CreatedUserId: {type: String}, UpdateUserId: {type: String}, ChildPageIds: [String], // Menu flags noDelete: { type: Boolean, default: false }, virtual: { type: Boolean, default: false }, // Sharing properties shared_users_view: [ObjectId], shared_users_update: [ObjectId], shared_users_manage: [ObjectId], }; } static scopes = [new ActiveScope] static async visible_by_user(user) { const user_root = await user.get_root_page() const user_root_children = await user_root.visible_flat_children(user) const view_only_trees = await this.find({ shared_users_view: user._id }) let view_only_children = [] for ( const tree of view_only_trees ) { if ( await tree.is_accessible_by(user) ) { view_only_children.push(tree) view_only_children = [...view_only_children, await tree.visible_flat_children(user)] } } const update_trees = await this.find({ shared_users_update: user._id }) let update_children = [] for ( const tree of update_trees ) { if ( await tree.is_accessible_by(user) ) { update_children.push(tree) update_children = [...update_children, await tree.visible_flat_children(user)] } } const manage_trees = await this.find({ shared_users_manage: user._id }) let manage_children = [] for ( const tree of manage_trees ) { if ( await tree.is_accessible_by(user) ) { manage_children.push(tree) manage_children = [...manage_children, await tree.visible_flat_children(user)] } } const all_children = [...user_root_children, ...view_only_children, ...update_children, ...manage_children] const unique_children = [] const seen_UUIDs = [] all_children.forEach(child => { if ( !seen_UUIDs.includes(child.UUID) ) { unique_children.push(child) seen_UUIDs.push(child.UUID) } }) return unique_children } async visible_flat_children(user) { const children = await this.childPages let visible = [] if ( children ) { for ( const child of children ) { if ( !(await child.is_accessible_by(user)) ) continue visible.push(child) visible = [...visible, ...(await child.visible_flat_children(user))] } } return visible } is_shared() { return this.shared_users_view.length > 0 || this.shared_users_update.length > 0 || this.shared_users_manage.length > 0 } // ================= RELATIONSHIPS ================= get user() { const User = this.models.get("auth:User") return this.belongs_to_one(User, "OrgUserId", "_id") } get nodes() { const Node = this.models.get("api:Node") return this.has_many(Node, "NodeIds", "UUID") } get childPages() { const Page = this.models.get("api:Page") return this.has_many(Page, "ChildPageIds", "UUID") } get parent() { const Parent = this.models.get("api:Page") return this.belongs_to_one(Parent, "ParentId", "UUID") } get view_users() { const User = this.models.get('auth:User') return this.has_many(User, 'shared_users_view', '_id') } get update_users() { const User = this.models.get('auth:User') return this.has_many(User, 'shared_users_update', '_id') } get manage_users() { const User = this.models.get('auth:User') return this.has_many(User, 'shared_users_manage', '_id') } // ================= 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}`) // Allow universal access if ( can_all ) return true // deny if blocked else if ( 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 else if ( mode === 'update' ) { if ( can_manage || can_update ) return true // If other permissions are explicitly set for this page, use those else if ( can_view ) return false } // manage can manage else if ( mode === 'manage' ) { if ( can_manage ) return true // If other permissions are explicitly set for this page, use those else if ( can_update || can_view ) return false } // allow universal access // deny blocked users // If there are no explicit permissions set for this node, check the parent const parent = await this.parent if ( parent ) return (await this.parent).is_accessible_by(user, mode) else return false } async access_level_for(user) { if ( await this.is_accessible_by(user, 'manage') ) return 'manage' else if ( await this.is_accessible_by(user, 'update') ) return 'update' else if ( await this.is_accessible_by(user, 'view') ) return 'view' else return false } async share_with(user, level = 'view') { if ( !['view', 'update', 'manage'].includes(level) ) { throw new Error(`Invalid share level: ${level}`) } // Remove existing sharing info await this.unshare_with(user) // Add the page to the user's permissions: user.allow(`page:${this.UUID}:${level}`) // Add the user to the appropriate access list this[`shared_users_${level}`].push(user._id) await this.save() await user.save() } async unshare_with(user) { // Remove this page from the user's permissions if ( 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}`) } // Remove the user from this page's access lists this.shared_users_view = this.shared_users_view.filter(x => String(x) !== user.id) this.shared_users_update = this.shared_users_update.filter(x => String(x) !== user.id) this.shared_users_manage = this.shared_users_manage.filter(x => String(x) !== user.id) await this.save() await user.save() } async get_menu_items() { // { // id: child.UUID, // name: child.is_shared() ? child.Name + ' ⁽ˢʰᵃʳᵉᵈ⁾' : child.Name, // shared: child.is_shared(), // children: await this._build_menu_object(child), // type: 'page', // } // Databases & Code Snips const children = [] const Database = this.models.get('api:db:Database') const dbs = await Database.find({ Active: true, PageId: this.UUID, }) for ( const db of dbs ) { children.push({ id: db.PageId, node_id: db.NodeId, children: [], type: 'db', name: db.Name, }) } const Codium = this.models.get('api:Codium') const codiums = await Codium.find({ PageId: this.UUID, }) for ( const codium of codiums ) { children.push({ id: codium.PageId, node_id: codium.NodeId, children: [], type: 'code', name: codium.code.slice(0, 25) + (codium.code.length > 25 ? '...' : ''), }) } return children } } module.exports = exports = Page;