@webda/core
Version:
Expose API with Lambda
526 lines • 20.9 kB
JavaScript
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