diff --git a/app/models/api/Page.model.js b/app/models/api/Page.model.js index 415d2c0..98b38dc 100644 --- a/app/models/api/Page.model.js +++ b/app/models/api/Page.model.js @@ -11,323 +11,341 @@ const { PageType } = require('../../enum'); * 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() { - 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_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 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 - } + 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() { + 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_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 // FIXME need to track this w/ the file boxes + 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;