const VersionedModel = require('../VersionedModel') const { ObjectId } = require('mongodb') const uuid = require('uuid/v4') const ActiveScope = require('../scopes/Active.scope') const { PageType } = require('../../enum'); /* * Page Model * ------------------------------------------------------------- * Put some description here! */ class Page extends VersionedModel { static get services() { return [...super.services, 'models']; } static get schema() { // Return a flitter-orm schema here. return { ...super.schema, 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], PageType: {type: String, default: PageType.Note}, // PageType AdditionalData: Object, // 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() { // TODO: public user sharing... 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") const node_promise = this.has_many(Node, "NodeIds", "UUID") if ( this.is_a_version() ) { // return the nodes for this version! return (async () => { const nodes = await node_promise const version_data = this.raw_version_data() const return_nodes = [] if ( version_data?.node_version_nums ) { const node_x_version_num = {} for ( const item of version_data.node_version_nums ) { node_x_version_num[item.NodeId] = item.version_num } for ( const node of nodes ) { if ( node_x_version_num[node.UUID] ) { return_nodes.push(await node.as_version(node_x_version_num[node.UUID])) } else { return_nodes.push(node) } } } return return_nodes })() } else { return node_promise } } 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 ================= async is_accessible_by(user, mode = 'view') { 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 ( 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 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_public(current_user, level = 'view') { const PublicUserPermission = this.models.get('auth:PublicUserPermission') if ( !['view', 'update', 'manage'].includes(level) ) { throw new Error(`Invalid share level: ${level}`) } const possible_grants = [':view', ':manage', ':update', ''].map(x => `page:${this.UUID}${x}`) // Remove existing sharing info await PublicUserPermission.deleteMany({ permission: { $in: possible_grants, }, }) // Create the new sharing level const share = new PublicUserPermission({ associated_user_id: this.OrgUserId, permission: `page:${this.UUID}:${level}`, }) await this.version_save(`Shared publicly (${level} access)`, current_user.id) await share.save() } async unshare_public(current_user) { const PublicUserPermission = this.models.get('auth:PublicUserPermission') const possible_grants = [':view', ':manage', ':update', ''].map(x => `page:${this.UUID}${x}`) // Remove existing sharing info await PublicUserPermission.deleteMany({ permission: { $in: possible_grants, }, }) await this.version_save(`Un-shared public access)`, current_user.id) } 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) // TODO replace user.uid with name of user when we support that await this.version_save(`Shared with ${user.uid} (${level} access)`, user.id) await user.save() } async unshare_with(user) { // Remove this page from the user's permissions if ( await user.can(`page:${this.UUID}`) ) user.disallow(`page:${this.UUID}`) for ( const level of ['view', 'update', 'manage'] ) { if ( await 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.version_save(`Unshared with ${user.uid}`, user.id) await user.save() } async get_menu_items(page_only) { if ( page_only ) return []; // { // 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 FileBox = this.models.get('api:files:FileBox') const boxes = await FileBox.find({ active: true, pageId: this.UUID, }) for ( const box of boxes ) { if ( box.parentUUID ) continue; // only show top-level children.push({ id: box.pageId, node_id: box.nodeId, children: [], type: 'file_box', name: box.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 } async cast_to_version_data() { const data = await super.cast_to_version_data() const node_version_nums = [] const nodes = await this.nodes for ( const node of nodes ) { node_version_nums.push({ NodeId: node.UUID, version_num: node.version_num }) } data.node_version_nums = node_version_nums return data } async revert_to_version(version_num, user_id = undefined) { const Node = this.models.get('api:Node') const reverted = await super.revert_to_version(version_num, user_id) const data = await this.get_version_data(version_num) // Revert the nodes to the given versions for ( const node_data of data.node_version_nums ) { const node = await Node.findOne({ UUID: node_data.NodeId }) if ( node ) { const reverted_node = await node.revert_to_version(node_data.version_num) await reverted_node.save() } } await reverted.save() return reverted } } module.exports = exports = Page;