Source

client/core/modules/directory.js

/**
 * 
 * ## Directory to handle users and groups
 * 
 * @namespace
 */
kiss.directory = {
	users: [],
	groups: [],
	collaborators: [],
	apiClients: [],
	roles: {},
	index: {},
	colors: {},

	/**
	 * Init the address book
	 * 
	 * @returns {Promise<boolean>} false if users or groups could not be loaded properly
	 */
	async init() {
		this._initRoles()

		if (kiss.session.isOnline()) {
			const success = await this._initUsersAndGroups()
			if (!success) return false

			this._initSubscriptions()
		}
		else {
			this._initOfflineUsersAndGroups()
		}
		return true
	},

	/**
	 * Init or reset the address book
	 * 
	 * @private
	 * @ignore
	 * @returns {boolean} false if users, groups, collaborators or API clients could not be loaded properly
	 */
	async _initUsersAndGroups() {
		this.users = []
		this.groups = []
		this.collaborators = []
		this.apiClients = []

		await this._loadUsers()
		if (!this.users) return false
        
		await this._loadGroups()
		if (!this.groups) return false

		await this._loadCollaborators()
		if (!this.collaborators) return false

		await this._loadApiClients()
		if (!this.apiClients) return false

		this._buildIndex()
		return true
	},

	/**
	 * Init or reset the address book for offline use
	 * 
	 * @private
	 * @ignore
	 */
	_initOfflineUsersAndGroups() {
		this.users = [{
			id: kiss.tools.uid(),
			email: "contact@airprocess.com",
			firstName: "Contact",
			lastName: "AirProcess",
			invitedBy: [],
			isCollaboratorOf: [],
			isInvite: false,
			active: true
		}]

		this.groups = [{
			id: kiss.tools.uid(),
			icon: "fas fa-users",
			color: "#00aaee",
			name: "Managers",
			users: ["contact@airprocess.com"]
		}]

		this._buildIndex()
	},

	/**
	 * Build a directory index for instant entry search
	 * 
	 * @private
	 * @ignore
	 */
	_buildIndex() {
		this.index = {}
		this.users.forEach(user => this.index[user.email] = user)
		this.groups.forEach(group => this.index[group.id] = group)
		this.apiClients.forEach(client => this.index[client.id] = client)

		// this.directory = []
		// this.directory = this.users.concat(this.groups)
	},

	/**
	 * Init special directory roles
	 * 
	 * @private
	 * @ignore
	 */
	_initRoles() {
		kiss.directory.roles.everyone = {
			type: "role",
			label: txtTitleCase("everyone"),
			value: "*"
		}

		kiss.directory.roles.authenticated = {
			type: "role",
			label: txtTitleCase("authenticated users"),
			value: "$authenticated"
		}

		kiss.directory.roles.creator = {
			type: "role",
			label: txtTitleCase("the creator of the record"),
			value: "$creator"
		}

		kiss.directory.roles.userId = {
			type: "role",
			label: txtTitleCase("connected user"),
			value: "$userId"
		}

		kiss.directory.roles.nobody = {
			type: "role",
			label: txtTitleCase("nobody"),
			value: "$nobody"
		}
	},

	/**
	 * Subscribe the directory to react to changes
	 * 
	 * @private
	 * @ignore
	 */
	_initSubscriptions() {
		if (this.subscriptions) return

		this.subscriptions = [
			// USERS
			subscribe("EVT_DB_INSERT:USER", (msgData) => {
				this.addUser(msgData.data)
				this._buildIndex()
			}),

			subscribe("EVT_DB_UPDATE:USER", (msgData) => {
				this.updateUser(msgData.id, msgData.data)
			}),

			subscribe("EVT_DB_DELETE:USER", (msgData) => {
				this.deleteUser(msgData.id)
				this._buildIndex()
			}),

			// GROUPS
			subscribe("EVT_DB_INSERT:GROUP", (msgData) => {
				this.addGroup(msgData.data)
				this._buildIndex()
			}),

			subscribe("EVT_DB_UPDATE:GROUP", (msgData) => {
				this.updateGroup(msgData.id, msgData.data)
			}),

			subscribe("EVT_DB_DELETE:GROUP", (msgData) => {
				this.deleteGroup(msgData.id)
				this._buildIndex()
			}),

			// API CLIENT
			subscribe("EVT_DB_INSERT:APICLIENT", (msgData) => {
				this.addApiClient(msgData.data)
				this._buildIndex()
			}),

			subscribe("EVT_DB_UPDATE:APICLIENT", (msgData) => {
				this.updateApiClient(msgData.id, msgData.data)
			}),

			subscribe("EVT_DB_DELETE:APICLIENT", (msgData) => {
				this.deleteApiClient(msgData.id)
				this._buildIndex()
			}),

			// COLLABORATION PROCESS
			subscribe("EVT_COLLABORATION:SENT", async () => {
				await this._initUsersAndGroups()
				kiss.pubsub.publish("EVT_DIRECTORY_UPDATED")
			}),

			subscribe("EVT_COLLABORATION:RECEIVED", async () => {
				kiss.pubsub.publish("EVT_DIRECTORY_UPDATED")
			}),

			subscribe("EVT_COLLABORATION:ACCEPTED", async () => {
				await this._initUsersAndGroups()
				kiss.pubsub.publish("EVT_DIRECTORY_UPDATED")
			}),

			subscribe("EVT_COLLABORATION:REJECTED", async () => {
				await this._initUsersAndGroups()
				kiss.pubsub.publish("EVT_DIRECTORY_UPDATED")
			}),            
            
			subscribe("EVT_COLLABORATION:STOPPED", async () => {
				await this._initUsersAndGroups()
				kiss.pubsub.publish("EVT_DIRECTORY_UPDATED")
			})
		]
	},

	/**
	 * Add a user
	 * 
	 * @param {object} user 
	 */
	addUser(user) {
		const hasUser = kiss.directory.users.find(existingUser => existingUser.id == user.id)
		if (hasUser) return
		kiss.directory.users.push(user)
	},

	/**
	 * Update a user
	 * 
	 * @param {string} userId
	 * @param {object} update 
	 */
	updateUser(userId, update) {
		let user = kiss.directory.users.get(userId)
		Object.assign(user, update)
	},

	/**
	 * Delete a user
	 * 
	 * @param {string} userId
	 */
	deleteUser(userId) {
		this.users = this.users.filter(user => user.id != userId)
	},    

	/**
	 * Add a group
	 * 
	 * @param {object} group
	 */
	addGroup(group) {
		const hasGroup = kiss.directory.groups.find(existingGroup => existingGroup.id == group.id)
		if (hasGroup) return
		kiss.directory.groups.push(group)
	},    

	/**
	 * Update a group
	 * 
	 * @param {string} groupId 
	 * @param {object} update 
	 */
	updateGroup(groupId, update) {
		let group = kiss.directory.groups.get(groupId)
		Object.assign(group, update)
	},

	/**
	 * Delete a group
	 * 
	 * @param {string} groupId 
	 */
	deleteGroup(groupId) {
		this.groups = this.groups.filter(group => group.id != groupId)
	},

	/**
	 * Add an API client
	 * 
	 * @param {object} client 
	 */
	addApiClient(client) {
		const hasClient = kiss.directory.apiClients.find(existingClient => existingClient.id == client.id)
		if (hasClient) return
		kiss.directory.apiClients.push(client)
	},

	/**
	 * Update an API client
	 * 
	 * @param {string} clientId
	 * @param {object} update 
	 */
	updateApiClient(clientId, update) {
		let client = kiss.directory.apiClients.get(clientId)
		Object.assign(client, update)
	},

	/**
	 * Delete an API client
	 * 
	 * @param {string} clientId
	 */
	deleteApiClient(clientId) {
		this.apiClients = this.apiClients.filter(client => client.id != clientId)
	},

	/**
	 * Load active account users
	 * 
	 * @private
	 * @async
	 * @returns {object[]} Array of users or false
	 */
	async _loadUsers() {
		this.users = await kiss.ajax.request({
			url: "/getUsers"
		})
		return this.users
	},

	/**
	 * Load account groups
	 * 
	 * @private
	 * @async
	 * @returns {object[]} Array of groups or false
	 */
	async _loadGroups() {
		this.groups = await kiss.app.collections.group.find()
		return this.groups
	},

	/**
	 * Load account collaborators
	 * 
	 * @private
	 * @async
	 * @returns {object[]} Array of collaborators or false
	 */
	async _loadCollaborators() {
		this.collaborators = await kiss.ajax.request({
			url: "/getCollaborators"
		})
		return this.collaborators
	},

	/**
	 * Load API clients
	 * 
	 * @private
	 * @async
	 * @returns {object[]} Array of API clients or false
	 */
	async _loadApiClients() {
		this.apiClients = await kiss.ajax.request({
			url: "/getApiClients"
		})
		return this.apiClients
	},

	/**
	 * Get a user or a group, given its email (for users) or id (for groups)
	 * 
	 * @param {string} entryId
	 * @returns {}
	 */
	getEntry(entryId) {
		return this.index[entryId]
	},

	/**
	 * Get a list of users and groups, given their ids
	 * 
	 * @param {string[]} entryId - Array of ids
	 * @param entryIds
	 * @returns {object[]} Array of entries
	 */
	getEntries(entryIds) {
		return entryIds.map(id => kiss.directory.index[id]).filter(entry => !!entry)
	},

	/**
	 * Returns the user name
	 * 
	 * @param {string} userId 
	 * @returns {string}
	 */
	getEntryName(userId) {
		const entry = kiss.directory.getEntry(userId)

		if (!entry) return userId

		if (entry.firstName && entry.lastName) {
			// It's a user
			return entry.firstName + " " + entry.lastName
            
		} else {
			// It's a group
			if (entry.users) {
				return entry.name
			}
			else if (entry.name) {
				// It's an API client
				return entry.name
			}

			return entry.email
		}
	},

	/**
	 * Get a list of user and group names, given their ids
	 * 
	 * @param {string[]} entryId - Array of ids
	 * @param entryIds
	 * @returns {string[]} Array of entry names
	 */
	getEntryNames(entryIds) {
		entryIds = [].concat(entryIds)
		return entryIds.map(this.getEntryName)
	},

	/**
	 * Returns all the names by which the user can be recognized into ACL lists.
	 * 
	 * @param {string} userId 
	 * @returns {string[]}
	 */
	getUserACL(userId) {
		let userACL = ["*"]

		if (kiss.session.isOnline()) {
			// Online
			userACL = userACL.concat(userId)
			if (kiss.session.isAuthenticated()) userACL = userACL.concat("$authenticated")
		}
		else {
			// Offline
			userACL = userACL.concat("$authenticated", "anonymous")
		}

		this.groups.forEach(group => {
			if (group.users.includes(userId)) userACL.push(group.id)
		})
		return userACL
	},

	/**
	 * Get the user initials
	 * 
	 * @param {object} user 
	 * @returns {string} The initials
	 * 
	 * @example
	 * const initials = kiss.directory.getUserInitials("bob@airprocess.com")
	 * console.log(initials) // "DG"
	 */
	getUserInitials(user) {
		if (!user.firstName || !user.lastName) return "??"
		return (user.firstName[0] + user.lastName[0]).toUpperCase()
	},

	/**
	 * Get the entry color (randomly assigned at startup)
	 * 
	 * @param {string} userId 
	 * @returns {string} The hex color code
	 * 
	 * @example
	 * const userColor = kiss.directory.getEntryColor("bob@airprocess.com")
	 * console.log(userColor) // "#00aaee"
	 */
	getEntryColor(userId) {
		let userColor = kiss.directory.colors[userId]
		if (userColor) return userColor

		userColor = kiss.tools.getRandomColor(0, 20)
		kiss.directory.colors[userId] = userColor
		return userColor
	},

	/**
	 * Get users
	 * 
	 * @param {object} config
	 * @param {string} config.sortBy - "firstName" | "lastName" (default)
	 * @param {string} config.nameOrder - "firstName" | "lastName" (default)
	 * @param {string} config.sortOrder - "asc" (default) | "desc"
	 * @param {boolean} config.onlyActiveUsers - true to filter out inactive users
	 * @returns {object[]} Array of users
	 */
	getUsers(config = {
		sortBy: "lastName",
		nameOrder: "lastName",
		sortOrder: "asc",
		onlyActiveUsers: false
	}) {
		const compareFunction = (config.sortBy == "firstName") ? this._sortByFirstName : this._sortByLastName

		const users = kiss.directory.users
			.filter(user => {
				if (config.onlyActiveUsers == false) return true
				return user.active !== false
			})
			.map(user => {
				return {
					type: "user",
					id: user.email,
					isInvite: user.isInvite,
					isOwner: user.isOwner,
					firstName: user.firstName || "",
					lastName: user.lastName || "",
					email: user.email,
					name: (user.firstName && user.lastName) ?
						((config.nameOrder == "firstName") ?
							(user.firstName + " " + user.lastName) :
							(user.lastName + " " + user.firstName)) : user.email
				}
			})
			.sort(compareFunction)

		if (config.sortOrder == "desc") return users.reverse()
		return users
	},

	/**
	 * Get groups
	 * 
	 * @param {string} sortOrder - "asc" (default) | "desc"
	 * @returns {object[]} Array of groups
	 */
	getGroups(sortOrder = "asc") {
		const groups = kiss.directory.groups
			.map(group => {
				return {
					type: "group",
					id: group.id,
					name: group.name
				}
			})
			.sort(this._sortByName)

		if (sortOrder == "desc") return groups.reverse()
		return groups
	},

	/**
	 * Get API clients
	 * 
	 * @returns {object[]} Array of API clients
	 */
	getApiClients() {
		const apiClients = kiss.directory.apiClients
			.map(client => {
				return {
					type: "api",
					id: client.id,
					name: client.name
				}
			})
			.sort(this._sortByName)

		return apiClients
	},    

	/**
	 * Sort by user firstName
	 * 
	 * @private
	 * @ignore
	 */
	_sortByFirstName(a, b) {
		if (a.firstName.toLowerCase() < b.firstName.toLowerCase()) return -1
		if (a.firstName.toLowerCase() > b.firstName.toLowerCase()) return 1
		return 0
	},

	/**
	 * Sort by user lastName
	 * 
	 * @private
	 * @ignore
	 */
	_sortByLastName(a, b) {
		if (a.lastName.toLowerCase() < b.lastName.toLowerCase()) return -1
		if (a.lastName.toLowerCase() > b.lastName.toLowerCase()) return 1
		return 0
	},

	/**
	 * Sort by group name
	 * 
	 * @private
	 * @ignore
	 */
	_sortByName(a, b) {
		if (a.name.toLowerCase() < b.name.toLowerCase()) return -1
		if (a.name.toLowerCase() > b.name.toLowerCase()) return 1
		return 0
	},

	/**
	 * Dialog to change the user first name and last name
	 * 
	 * @param {string} userId 
	 */
	async editUsername(userId) {
		const user = await kiss.app.collections.user.findOne(userId)
		const initialFirstName = user.firstName
		const initialLastName = user.lastName

		createPanel({
			id: "edit-user-infos",
			title: txtTitleCase("#title edit name"),
			icon: "fas fa-edit",
			layout: "vertical",
			align: "center",
			verticalAlign: "center",
			modal: true,
			closable: true,
			draggable: true,
			headerStyle: "flat",
			padding: "2rem",
			width: "50rem",

			defaultConfig: {
				labelPosition: "top",
				width: "100%"
			},

			items: [
				{
					id: "edit-firstName",
					type: "text",
					label: txtTitleCase("first name"),
					value: user.firstName,
					required: true,
					min: 2,
					max: 50
				},
				{
					id: "edit-lastName",
					type: "text",
					label: txtTitleCase("last name"),
					value: user.lastName,
					required: true,
					min: 2,
					max: 50
				},
				{
					type: "button",
					text: txtTitleCase("#button edit name"),
					icon: "fas fa-check",
					height: 40,
					margin: "2rem 0 1rem 0",

					action: () => {
						const success = $("edit-user-infos").validate()
						if (!success) return

						const firstName = $("edit-firstName").getValue()
						const lastName = $("edit-lastName").getValue()
                        
						// No change: exit
						if (initialFirstName == firstName && initialLastName == lastName) {
							$("edit-user-infos").close()
							return true
						}

						// Change: confirm
						createDialog({
							type: "danger",
							title: txtTitleCase("#title edit name"),
							message: txtTitleCase("#confirm edit name", null, {
								firstName,
								lastName
							}),
							action: async () => {
								$("edit-user-infos").close()
								await user.update({
									firstName,
									lastName
								})

								// Update session informations
								localStorage.setItem("session-firstName", firstName)
								localStorage.setItem("session-lastName", lastName)
								kiss.pubsub.publish("EVT_USERNAME_UPDATED")
							}
						})
					}
				}
			]
		}).render()
	},

	/**
	 * Convert a list of recipients, users or groups, into a list of emails.
	 * Recipients given as uid are groups, and they are exploded into multiple emails.
	 * 
	 * @param {string[]} recipients 
	 * @returns {string} List of emails, separated by a comma
	 * 
	 * @example
	 * const users = ["bob@airprocess.com", "d266f871-3624-4cb7-9efa-b1c742a0fefd"] // Second item is a group id
	 * 
	 * const recipients = kiss.directory.toEmail(users)
	 * console.log(recipients) // "bob@airprocess.com, john@airprocess.com, drakkhen@airprocess.com"
	 */
	toEmail(recipients) {
		const groups = this.groups

		recipients = [].concat(recipients)
		recipients = recipients.map(recipient => {
			if (!kiss.tools.isUid(recipient)) return recipient

			const group = groups.find(group => group.id == recipient)
			if (!group) return []
            
			const groupUsers = group.users
			if (groupUsers) return groupUsers
			return []
		})

		// Flatten + unique + remove "*" from recipients + separate recipients by a comma
		recipients = recipients.flat()
		recipients = recipients.unique()
		recipients = recipients.filter(recipient => recipient != "*")
		return recipients.join(",")
	}
}