UNPKG

@webda/core

Version:

Expose API with Lambda

526 lines 20.9 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { Ident, WebdaError } from "../index.js"; import { Inject, Service, ServiceParameters } from "./service.js"; /** * */ export class InvitationParameters extends ServiceParameters { constructor(params) { super(params); this.authenticationService ?? (this.authenticationService = "Authentication"); if (typeof this.mapFields === "string") { this.mapFields = this.mapFields.split(","); } this.mapFields ?? (this.mapFields = []); this.multiple ?? (this.multiple = true); if (!this.attribute.startsWith("_")) { throw new Error(`Attribute '${this.attribute}' needs to start with a _ to be only modified by server`); } this.pendingAttribute ?? (this.pendingAttribute = `${this.attribute}Pending`); if (!this.pendingAttribute.startsWith("_")) { throw new Error(`Attribute '${this.pendingAttribute}' needs to start with a _ to be only modified by server`); } if (this.autoAccept && !this.multiple) { throw new Error(`You cannot have multiple=false with autoAccept=true`); } } } /** * Allow to use invitation on AclModel * * Once you add a new ACE with a user, it will invite * and act as Mapper * * @WebdaModda */ export default class InvitationService extends Service { /** * @inheritdoc */ loadParameters(params) { return new InvitationParameters(params); } /** * @inheritdoc */ resolve() { super.resolve(); // Register this.authenticationService.on("Authentication.Register", (evt) => this.registrationListener(evt)); this.model = this.getWebda().getModel(this.parameters.model); const url = this.getParameters().url || `/${this.getWebda().getApplication().getModelPlural(this.parameters.model)}`; // Register routes this.addRoute(`${url}/{uuid}/invitations`, ["GET", "POST", "PUT", "DELETE"], this.invite, { tags: [this.getWebda().getApplication().getModelPlural(this.model.getIdentifier())], summary: "Invite users to this object", get: { summary: "Retrieve all pending invitations", responses: { "200": { description: "List of pending invitations", content: { "application/json": { schema: { type: "object", additionalProperties: { type: "object" } } } } }, "403": { description: "Forbidden" } } }, put: { summary: "Accept/Refuse an invitation", requestBody: { content: { "application/json": { schema: { type: "object", properties: { accept: { type: "boolean" } } } } } }, responses: { "204": { description: "Operation successful" }, "410": { description: "Invitation is gone" } } }, delete: { summary: "Remove invitation", requestBody: { content: { "application/json": { schema: { type: "object", properties: { users: { type: "array", items: { type: "string" } }, idents: { type: "array", items: { type: "string" } }, metadata: { type: "object" }, notification: { type: "object" } } } } } }, responses: { "204": {}, "403": { description: "Forbidden" } } }, post: { summary: "Invite users", requestBody: { content: { "application/json": { schema: { type: "object", properties: { users: { type: "array", items: { type: "string" } }, idents: { type: "array", items: { type: "string" } }, metadata: { type: "object" }, notification: { type: "object" } } } } } }, responses: { "204": {}, "403": { description: "Forbidden" } } } }); return this; } /** * @inheritdoc */ async init() { await super.init(); if (this.parameters.notification && !(await this.notificationService.hasNotification(this.parameters.notification))) { throw new Error(`Email template should exist`); } return this; } /** * Accept or refuse for an invitation * @param ctx * @param model */ async answerInvitation(ctx, model) { let body = await ctx.getInput(); // Invitation on the model is gone if (model === undefined) { await this.removeInvitationFromUser(ctx.getCurrentUserId(), ctx.getParameters().uuid); throw new WebdaError.Gone("Invitation is gone"); } const user = await ctx.getCurrentUser(); let metadata = undefined; user.getIdents().forEach(i => { if (model[this.parameters.pendingAttribute][`ident_${i.uuid}`]) { metadata = model[this.parameters.pendingAttribute][`ident_${i.uuid}`]; delete model[this.parameters.pendingAttribute][`ident_${i.uuid}`]; } }); if (model[this.parameters.pendingAttribute][`user_${user.getUuid()}`]) { metadata = model[this.parameters.pendingAttribute][`user_${user.getUuid()}`]; delete model[this.parameters.pendingAttribute][`user_${user.getUuid()}`]; } if (metadata && body.accept) { model[this.parameters.attribute][user.getUuid()] = metadata; } await this.updateModel(model); this.emit("Invitation.Accepted", { metadata, model, context: ctx, accept: body.accept }); } /** * Update Model with pending and attribute * * @param model */ async updateModel(model) { await model.patch({ [this.parameters.attribute]: model[this.parameters.attribute], [this.parameters.pendingAttribute]: model[this.parameters.pendingAttribute] }); } /** * Uninvite from previous invitations * @param ctx * @param model */ async uninvite(ctx, model) { const body = await ctx.getInput(); body.users ?? (body.users = []); body.idents ?? (body.idents = []); const promises = []; let invitedIdents = []; let invitedUsers = []; for (const ident of body.idents) { if (model[this.parameters.pendingAttribute][`ident_${ident}`]) { // Remove from pending on the object delete model[this.parameters.pendingAttribute][`ident_${ident}`]; // Remove from pending from the invitation store promises.push(this.invitationStore.removeAttribute(`${ident}_${this.getName()}`, this.getInvitationAttribute(model.getUuid()))); invitedIdents.push(ident); } else { // Remove user promises.push((async () => { const id = await this.authenticationService.getIdentStore().get(ident); if (id && id.getUser()) { delete model[this.parameters.attribute][id.getUser()]; await this.removeInvitationFromUser(id.getUser().toString(), model.getUuid()); invitedUsers.push(id.getUser().toString()); } })()); } } for (const user of body.users) { if (model[this.parameters.pendingAttribute][`user_${user}`]) { delete model[this.parameters.pendingAttribute][`user_${user}`]; } else if (model[this.parameters.attribute][user]) { delete model[this.parameters.attribute][user]; } promises.push(this.removeInvitationFromUser(user, model.getUuid())); invitedUsers.push(user); } await Promise.all(promises); // await this.updateModel(model); this.emit("Invitation.Removed", { metadata: body.metadata, users: invitedUsers, idents: invitedIdents, model, context: ctx }); } /** * Remove a model invitation from user * @param user */ async removeInvitationFromUser(user, model) { let userModel = await this.authenticationService.getUserStore().get(user); let index = 0; for (let invit of userModel[this.parameters.mapAttribute] || []) { if (invit.model === model) { await this.authenticationService .getUserStore() .deleteItemFromCollection(user, this.parameters.mapAttribute, index, model, "model"); return; } index++; } } /** * Handle invitations all methods * * @param ctx * @returns */ async invite(ctx) { var _a, _b, _c; let model = await this.model.ref(ctx.getParameters().uuid).get(ctx); if (ctx.getHttpContext().getMethod() === "PUT") { return this.answerInvitation(ctx, model); } let inviter = await ctx.getCurrentUser(); if (!model) { throw new WebdaError.NotFound("Model not found"); } model[_a = this.parameters.attribute] ?? (model[_a] = {}); model[_b = this.parameters.pendingAttribute] ?? (model[_b] = {}); if (ctx.getHttpContext().getMethod() === "DELETE") { await model.checkAct(ctx, "uninvite"); return this.uninvite(ctx, model); } await model.checkAct(ctx, "invite"); // Retrieve invitation useful when invitation are hidden with a __ if (ctx.getHttpContext().getMethod() === "GET") { ctx.write(model[this.parameters.pendingAttribute] || {}); return; } const body = await ctx.getRequestBody(); // For each ident const identsStore = this.authenticationService.getIdentStore(); // Load all idents with orignal const invitations = await Promise.all((body.idents || []).map(async (i) => ({ ident: await identsStore.get(i), invitation: i }))); let invitedIdents = []; let invitedUsers = []; let promises = []; const metadata = {}; this.parameters.mapFields.forEach(f => (metadata[f] = model[f])); for (const invitation of invitations) { // User is known if (invitation.ident) { if (this.parameters.autoAccept) { model[this.parameters.attribute][invitation.ident.getUser()] = body.metadata; } else if (!model[this.parameters.attribute][invitation.ident.getUser()]) { // Add to the user model[this.parameters.pendingAttribute][`user_${invitation.ident.getUser()}`] = body.metadata; } promises.push((async () => { const user = await this.authenticationService.getUserStore().get(invitation.ident.getUser().toString()); invitedUsers.push(user); await this.addInvitationToUser(model, user, inviter, metadata, body.notification); })()); continue; } // User is unknown to the platform invitedIdents.push(invitation.invitation); let invitUuid = `${invitation.invitation}_${this.getName()}`; model[_c = this.parameters.pendingAttribute] ?? (model[_c] = {}); model[this.parameters.pendingAttribute][`ident_${invitation.invitation}`] = body.metadata; const invitInfo = { inviter: inviter.toPublicEntry(), metadata: body.metadata }; if (await this.invitationStore.exists(invitUuid)) { promises.push(this.invitationStore.setAttribute(invitUuid, this.getInvitationAttribute(model.getUuid()), invitInfo)); } else { promises.push(this.invitationStore.save({ uuid: invitUuid, [this.getInvitationAttribute(model.getUuid())]: invitInfo })); } // Notify ident let ident = invitation.invitation.split("_"); await this.sendNotification(Ident.init(ident.pop(), ident.join("_")), { model, metadata, notification: body.notification, inviter: inviter.toPublicEntry(), pending: !this.parameters.autoAccept, registered: true }); } // Check user direct invite body.users ?? (body.users = []); promises.push(...body.users.map(async (u) => { let user = await this.authenticationService.getUserStore().get(u); if (!user) { return; } if (this.parameters.autoAccept) { model[this.parameters.attribute][u] = body.metadata; } else if (!model[this.parameters.attribute][u]) { model[this.parameters.pendingAttribute][`user_${u}`] = body.metadata; } invitedUsers.push(user); await this.addInvitationToUser(model, user, inviter, metadata, body.notification); })); await Promise.all(promises); // Update model now await this.updateModel(model); this.emit("Invitation.Sent", { metadata: body.metadata, notification: body.notification, users: invitedUsers, idents: invitedIdents, model, context: ctx }); } async addInvitationToUser(model, user, inviter, metadata, notification = {}) { if ((user[this.parameters.mapAttribute] || []).filter(p => p.model === model.getUuid()).length) { return; } await this.authenticationService .getUserStore() .upsertItemToCollection(user.getUuid(), this.parameters.mapAttribute, { model: model.getUuid(), metadata, inviter: inviter.toPublicEntry(), pending: !this.parameters.autoAccept }); // Notify user await this.sendNotification(user, { model, metadata, notification, inviter: inviter.toPublicEntry(), pending: !this.parameters.autoAccept, registered: true }); } async sendNotification(user, replacements) { if (!this.parameters.notification) { return; } await this.notificationService.sendNotification(user, this.parameters.notification, replacements); } /** * Return which attribute would be used to store the invitation on ident invitation object * @param uuid * @returns */ getInvitationAttribute(uuid) { return `invit_${uuid}`; } /** * When a user register with an invited idents, managed the whole invitation process * * @param evt * @returns */ async registrationListener(evt) { var _a, _b; let uuid = `${evt.identId}_${this.getName()}`; const invitations = await this.invitationStore.get(uuid); if (!invitations) { return; } this.log("DEBUG", `Resolving invitation for ident ${evt.identId}`); // Load all models invited too const infos = await Promise.all(Object.keys(invitations) .filter(k => k.startsWith("invit_")) .map(async (k) => ({ ...invitations[k], model: await this.model.ref(k.substring(6)).get() }))); // autoAccept is on so move to __acls const promises = []; // If autoAccept is false, just copy the pending invitation in the user (_a = evt.user)[_b = this.parameters.mapAttribute] ?? (_a[_b] = []); evt.user[this.parameters.mapAttribute].push(...infos .filter(m => m.model !== undefined) .map(m => { const metadata = {}; this.parameters.mapFields.forEach(f => (metadata[f] = m.model[f])); return { model: m.model.getUuid(), metadata, inviter: m.inviter, pending: !this.parameters.autoAccept }; })); if (this.parameters.autoAccept) { for (const info of infos) { if (!info.model) { continue; } // Set the metadata info.model[this.parameters.attribute][evt.user.getUuid()] = info.metadata; // Remove pending delete info.model[this.parameters.pendingAttribute][`ident_${evt.identId}`]; promises.push(this.updateModel(info.model)); } } promises.push(invitations.delete()); await Promise.all(promises); } } __decorate([ Inject("params:authenticationService") ], InvitationService.prototype, "authenticationService", void 0); __decorate([ Inject("params:notificationService", true) ], InvitationService.prototype, "notificationService", void 0); __decorate([ Inject("params:invitationStore") ], InvitationService.prototype, "invitationStore", void 0); //# sourceMappingURL=invitationservice.js.map