@imenam/mcp-organizer 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +529 -2
- package/dist/shared/logic/first-account.d.ts +13 -0
- package/dist/shared/logic/first-account.js +15 -0
- package/dist/shared/logic/labels.d.ts +5 -0
- package/dist/shared/logic/labels.js +17 -0
- package/dist/shared/logic/positions.d.ts +9 -0
- package/dist/shared/logic/positions.js +19 -0
- package/dist/shared/logic/spaces.d.ts +13 -0
- package/dist/shared/logic/spaces.js +48 -0
- package/dist/shared/logic/test-summary.d.ts +28 -0
- package/dist/shared/logic/test-summary.js +35 -0
- package/dist/shared/schemas/agent-api.schema.d.ts +268 -0
- package/dist/shared/schemas/agent-api.schema.js +94 -0
- package/dist/shared/schemas/agent-tools.schema.d.ts +724 -0
- package/dist/shared/schemas/agent-tools.schema.js +165 -0
- package/dist/shared/schemas/api.schema.d.ts +249 -0
- package/dist/shared/schemas/api.schema.js +116 -0
- package/dist/shared/schemas/entities.schema.d.ts +472 -0
- package/dist/shared/schemas/entities.schema.js +262 -0
- package/dist/shared/schemas/enums.schema.d.ts +135 -0
- package/dist/shared/schemas/enums.schema.js +58 -0
- package/dist/shared/schemas/gui-api.schema.d.ts +53 -0
- package/dist/shared/schemas/gui-api.schema.js +20 -0
- package/dist/shared/schemas/ideas-api.schema.d.ts +81 -0
- package/dist/shared/schemas/ideas-api.schema.js +40 -0
- package/dist/shared/schemas/notifications-api.schema.d.ts +87 -0
- package/dist/shared/schemas/notifications-api.schema.js +58 -0
- package/dist/shared/schemas/tasks-api.schema.d.ts +675 -0
- package/dist/shared/schemas/tasks-api.schema.js +225 -0
- package/dist/shared/schemas/tokens-api.schema.d.ts +106 -0
- package/dist/shared/schemas/tokens-api.schema.js +42 -0
- package/dist/shared/schemas/transcription-api.schema.d.ts +15 -0
- package/dist/shared/schemas/transcription-api.schema.js +12 -0
- package/dist/src/agent-api/checklist.d.ts +3 -0
- package/dist/src/agent-api/checklist.js +127 -0
- package/dist/src/agent-api/ideas.d.ts +3 -0
- package/dist/src/agent-api/ideas.js +93 -0
- package/dist/src/agent-api/requests.d.ts +3 -0
- package/dist/src/agent-api/requests.js +60 -0
- package/dist/src/agent-api/router.d.ts +3 -0
- package/dist/src/agent-api/router.js +40 -0
- package/dist/src/agent-api/tasks.d.ts +2 -0
- package/dist/src/agent-api/tasks.js +290 -0
- package/dist/src/agent-api/types.d.ts +41 -0
- package/dist/src/agent-api/types.js +14 -0
- package/dist/src/cli/backup.d.ts +12 -0
- package/dist/src/cli/backup.js +126 -0
- package/dist/src/cli/prompt.d.ts +7 -0
- package/dist/src/cli/prompt.js +40 -0
- package/dist/src/cli/set-user.d.ts +2 -0
- package/dist/src/cli/set-user.js +114 -0
- package/dist/src/cli/setup-mcp.d.ts +10 -0
- package/dist/src/cli/setup-mcp.js +110 -0
- package/dist/src/cli/setup.d.ts +39 -0
- package/dist/src/cli/setup.js +626 -0
- package/dist/src/core/accounts.d.ts +129 -0
- package/dist/src/core/accounts.js +284 -0
- package/dist/src/core/agent-tokens.d.ts +31 -0
- package/dist/src/core/agent-tokens.js +120 -0
- package/dist/src/core/agent.d.ts +71 -0
- package/dist/src/core/agent.js +358 -0
- package/dist/src/core/attachments.d.ts +21 -0
- package/dist/src/core/attachments.js +44 -0
- package/dist/src/core/data-dir.d.ts +5 -0
- package/dist/src/core/data-dir.js +11 -0
- package/dist/src/core/db.d.ts +13 -0
- package/dist/src/core/db.js +38 -0
- package/dist/src/core/env-file.d.ts +11 -0
- package/dist/src/core/env-file.js +36 -0
- package/dist/src/core/errors.d.ts +24 -0
- package/dist/src/core/errors.js +42 -0
- package/dist/src/core/history.d.ts +34 -0
- package/dist/src/core/history.js +279 -0
- package/dist/src/core/ideas.d.ts +16 -0
- package/dist/src/core/ideas.js +100 -0
- package/dist/src/core/ids.d.ts +2 -0
- package/dist/src/core/ids.js +5 -0
- package/dist/src/core/log-dir.d.ts +2 -0
- package/dist/src/core/log-dir.js +9 -0
- package/dist/src/core/logger.d.ts +14 -0
- package/dist/src/core/logger.js +37 -0
- package/dist/src/core/mail.d.ts +43 -0
- package/dist/src/core/mail.js +44 -0
- package/dist/src/core/mcp-entry.d.ts +20 -0
- package/dist/src/core/mcp-entry.js +15 -0
- package/dist/src/core/migrations.d.ts +18 -0
- package/dist/src/core/migrations.js +404 -0
- package/dist/src/core/native-sqlite.d.ts +2 -0
- package/dist/src/core/native-sqlite.js +23 -0
- package/dist/src/core/notifications.d.ts +46 -0
- package/dist/src/core/notifications.js +173 -0
- package/dist/src/core/project-root.d.ts +2 -0
- package/dist/src/core/project-root.js +13 -0
- package/dist/src/core/requests.d.ts +49 -0
- package/dist/src/core/requests.js +239 -0
- package/dist/src/core/snapshots.d.ts +32 -0
- package/dist/src/core/snapshots.js +35 -0
- package/dist/src/core/spaces.d.ts +66 -0
- package/dist/src/core/spaces.js +252 -0
- package/dist/src/core/store.d.ts +565 -0
- package/dist/src/core/store.js +2093 -0
- package/dist/src/core/task-history.d.ts +7 -0
- package/dist/src/core/task-history.js +41 -0
- package/dist/src/core/tasks.d.ts +58 -0
- package/dist/src/core/tasks.js +389 -0
- package/dist/src/core/test-tasks.d.ts +19 -0
- package/dist/src/core/test-tasks.js +135 -0
- package/dist/src/core/transcription.d.ts +45 -0
- package/dist/src/core/transcription.js +92 -0
- package/dist/src/deploy/env-render.d.ts +26 -0
- package/dist/src/deploy/env-render.js +98 -0
- package/dist/src/deploy/templates.d.ts +38 -0
- package/dist/src/deploy/templates.js +130 -0
- package/dist/src/gui-api.d.ts +37 -0
- package/dist/src/gui-api.js +383 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +100 -0
- package/dist/src/mcp/client.d.ts +23 -0
- package/dist/src/mcp/client.js +81 -0
- package/dist/src/mcp/start.d.ts +14 -0
- package/dist/src/mcp/start.js +104 -0
- package/dist/src/mcp/tools.d.ts +46 -0
- package/dist/src/mcp/tools.js +348 -0
- package/dist/src/server/app.d.ts +25 -0
- package/dist/src/server/app.js +290 -0
- package/dist/src/server/config.d.ts +23 -0
- package/dist/src/server/config.js +75 -0
- package/dist/src/server/limits.d.ts +2 -0
- package/dist/src/server/limits.js +4 -0
- package/dist/src/server/login-limiter.d.ts +36 -0
- package/dist/src/server/login-limiter.js +81 -0
- package/dist/src/server/start.d.ts +5 -0
- package/dist/src/server/start.js +88 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +16 -0
- package/gui/dist/assets/index-DwkXx4Q2.js +70 -0
- package/gui/dist/assets/index-lnvnkDdg.css +1 -0
- package/gui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/gui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/gui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/gui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/gui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/gui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/gui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/gui/dist/index.html +13 -0
- package/package.json +66 -4
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { ACCESS_LINK_TTL_MS, accessLinkPath, issueAccessLink, joinInvitedSpace, normalizeEmail } from "./accounts.js";
|
|
2
|
+
import { ForbiddenError, InvariantError, NotFoundError } from "./errors.js";
|
|
3
|
+
import { deriveKeyPrefix } from "../../shared/logic/spaces.js";
|
|
4
|
+
// --- Droits ---------------------------------------------------------------------
|
|
5
|
+
/** Espace dont `userId` est membre ; un non-membre reçoit la même réponse qu'un espace inexistant. */
|
|
6
|
+
export function requireMember(store, spaceId, userId) {
|
|
7
|
+
const space = store.findSpace(spaceId);
|
|
8
|
+
const membership = space ? store.findMembership(spaceId, userId) : null;
|
|
9
|
+
if (!space || !membership)
|
|
10
|
+
throw new NotFoundError("Espace introuvable, ou vous n'en êtes pas membre.");
|
|
11
|
+
return { space, membership };
|
|
12
|
+
}
|
|
13
|
+
/** Espace commun dont `userId` est le propriétaire. */
|
|
14
|
+
export function requireOwner(store, spaceId, userId, action) {
|
|
15
|
+
const { space, membership } = requireMember(store, spaceId, userId);
|
|
16
|
+
if (space.kind === "personal")
|
|
17
|
+
throw new ForbiddenError(`Impossible de ${action} : c'est un espace personnel.`);
|
|
18
|
+
if (membership.role !== "owner")
|
|
19
|
+
throw new ForbiddenError(`Seul le propriétaire de l'espace « ${space.name} » peut ${action}.`);
|
|
20
|
+
return space;
|
|
21
|
+
}
|
|
22
|
+
// --- Vues -----------------------------------------------------------------------
|
|
23
|
+
function summary(space, membership) {
|
|
24
|
+
return { id: space.id, name: space.name, kind: space.kind, keyPrefix: space.keyPrefix, ownerId: space.ownerId, role: membership.role };
|
|
25
|
+
}
|
|
26
|
+
function memberView(user, membership) {
|
|
27
|
+
return { userId: user.id, email: user.email, displayName: user.displayName, role: membership.role };
|
|
28
|
+
}
|
|
29
|
+
function invitationView(invitation, now) {
|
|
30
|
+
return {
|
|
31
|
+
id: invitation.id,
|
|
32
|
+
email: invitation.email,
|
|
33
|
+
createdAt: invitation.createdAt,
|
|
34
|
+
expiresAt: invitation.expiresAt,
|
|
35
|
+
expired: isExpired(invitation, now),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function isExpired(invitation, now) {
|
|
39
|
+
return Date.parse(invitation.expiresAt) <= now.getTime();
|
|
40
|
+
}
|
|
41
|
+
function invitationLink(token, expiresAt) {
|
|
42
|
+
return { path: accessLinkPath(token), expiresAt };
|
|
43
|
+
}
|
|
44
|
+
function userName(user) {
|
|
45
|
+
return user ? (user.displayName ?? user.email) : "un ancien membre";
|
|
46
|
+
}
|
|
47
|
+
// --- Espaces --------------------------------------------------------------------
|
|
48
|
+
/** Espaces de l'utilisateur : son espace personnel d'abord, puis les espaces communs par nom. */
|
|
49
|
+
export function listSpaces(store, userId) {
|
|
50
|
+
return store.listSpacesOf(userId).map((space) => summary(space, store.findMembership(space.id, userId)));
|
|
51
|
+
}
|
|
52
|
+
/** Crée un espace commun dont l'utilisateur est propriétaire ; le préfixe est dérivé du nom s'il est omis. */
|
|
53
|
+
export function createSharedSpace(store, userId, input) {
|
|
54
|
+
return store.mutate({ type: "user", userId }, (tx) => {
|
|
55
|
+
const space = tx.createSpace({ name: input.name, kind: "shared", ownerId: userId, keyPrefix: input.keyPrefix ?? deriveKeyPrefix(input.name) });
|
|
56
|
+
return summary(space, tx.store.findMembership(space.id, userId));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function renameSpace(store, userId, spaceId, name) {
|
|
60
|
+
return store.mutate({ type: "user", userId }, (tx) => {
|
|
61
|
+
requireOwner(tx.store, spaceId, userId, "renommer l'espace");
|
|
62
|
+
return summary(tx.renameSpace(spaceId, name), tx.store.findMembership(spaceId, userId));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Supprime un espace commun, son contenu et les fichiers de ses pièces jointes. */
|
|
66
|
+
export function deleteSpace(store, storage, userId, spaceId) {
|
|
67
|
+
const removed = store.mutate({ type: "user", userId }, (tx) => {
|
|
68
|
+
requireOwner(tx.store, spaceId, userId, "supprimer l'espace");
|
|
69
|
+
const attachments = tx.store.listAttachmentsInSpace(spaceId);
|
|
70
|
+
tx.deleteSpace(spaceId);
|
|
71
|
+
return attachments;
|
|
72
|
+
});
|
|
73
|
+
storage.remove(removed.map((attachment) => attachment.storagePath));
|
|
74
|
+
}
|
|
75
|
+
// --- Membres --------------------------------------------------------------------
|
|
76
|
+
/** Membres (propriétaire d'abord) et invitations en attente d'un espace, pour un de ses membres. */
|
|
77
|
+
export function listMembers(store, userId, spaceId, now = new Date()) {
|
|
78
|
+
const { space, membership } = requireMember(store, spaceId, userId);
|
|
79
|
+
const members = store
|
|
80
|
+
.listMemberships(spaceId)
|
|
81
|
+
.map((m) => memberView(store.getUser(m.userId), m))
|
|
82
|
+
.sort((a, b) => (a.role === b.role ? 0 : a.role === "owner" ? -1 : 1));
|
|
83
|
+
const invitations = store.listOpenInvitations(spaceId).map((invitation) => invitationView(invitation, now));
|
|
84
|
+
return { space: summary(space, membership), members, invitations };
|
|
85
|
+
}
|
|
86
|
+
/** Le propriétaire retire un membre d'un espace commun. */
|
|
87
|
+
export function removeMember(store, userId, spaceId, memberId) {
|
|
88
|
+
store.mutate({ type: "user", userId }, (tx) => {
|
|
89
|
+
const space = requireOwner(tx.store, spaceId, userId, "retirer un membre");
|
|
90
|
+
if (memberId === userId)
|
|
91
|
+
throw new InvariantError("Le propriétaire ne peut pas se retirer de son espace : supprimez l'espace à la place.");
|
|
92
|
+
if (!tx.store.isMember(spaceId, memberId))
|
|
93
|
+
throw new NotFoundError(`Cette personne n'est pas membre de l'espace « ${space.name} ».`);
|
|
94
|
+
tx.removeMember(spaceId, memberId);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** Un membre quitte un espace commun ; le propriétaire ne le peut pas. */
|
|
98
|
+
export function leaveSpace(store, userId, spaceId) {
|
|
99
|
+
store.mutate({ type: "user", userId }, (tx) => {
|
|
100
|
+
const { space, membership } = requireMember(tx.store, spaceId, userId);
|
|
101
|
+
if (space.kind === "personal")
|
|
102
|
+
throw new ForbiddenError("On ne quitte pas son espace personnel.");
|
|
103
|
+
if (membership.role === "owner")
|
|
104
|
+
throw new ForbiddenError(`Vous êtes propriétaire de « ${space.name} » : supprimez l'espace au lieu de le quitter.`);
|
|
105
|
+
tx.removeMember(spaceId, userId);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// --- Invitations ----------------------------------------------------------------
|
|
109
|
+
/** E-mail d'invitation ; sans URL publique, il ne peut pas contenir de lien et n'est pas envoyé. */
|
|
110
|
+
async function sendInvitationEmail(deps, to, inviter, space, link) {
|
|
111
|
+
const base = deps.publicUrl?.trim().replace(/\/+$/, "");
|
|
112
|
+
if (!base) {
|
|
113
|
+
return {
|
|
114
|
+
emailSent: false,
|
|
115
|
+
reason: "E-mail non envoyé : ORGANIZER_PUBLIC_URL n'est pas défini, le lien complet ne peut pas figurer dans le message. Transmettez le lien vous-même.",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const url = `${base}/${link.path}`;
|
|
119
|
+
const outcome = await deps.mailer.send({
|
|
120
|
+
to,
|
|
121
|
+
subject: `${inviter.displayName ?? inviter.email} vous invite dans « ${space.name} »`,
|
|
122
|
+
text: `${inviter.displayName ?? inviter.email} vous invite à rejoindre l'espace « ${space.name} » sur mcp-organizer.\n\n` +
|
|
123
|
+
`Pour créer votre compte, ouvrez ce lien (valable jusqu'au ${new Date(link.expiresAt).toLocaleDateString("fr-FR")}) :\n${url}\n`,
|
|
124
|
+
});
|
|
125
|
+
return outcome.sent ? { emailSent: true, reason: null } : { emailSent: false, reason: outcome.reason };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Le propriétaire invite une adresse. Un compte actif devient membre
|
|
129
|
+
* directement ; une adresse inconnue reçoit un compte `invited` ; un compte
|
|
130
|
+
* `invited` reçoit l'invitation en plus des siennes. Dans ces deux derniers
|
|
131
|
+
* cas, un lien d'activation propre à cette invitation est émis, renvoyé à
|
|
132
|
+
* l'invitant et envoyé par e-mail ; les liens des autres invitations du compte
|
|
133
|
+
* restent valables.
|
|
134
|
+
*/
|
|
135
|
+
export async function inviteToSpace(store, deps, userId, spaceId, rawEmail) {
|
|
136
|
+
const now = deps.now?.() ?? new Date();
|
|
137
|
+
const email = normalizeEmail(rawEmail);
|
|
138
|
+
const expiresAt = new Date(now.getTime() + ACCESS_LINK_TTL_MS).toISOString();
|
|
139
|
+
const outcome = store.mutate({ type: "user", userId }, (tx) => {
|
|
140
|
+
const space = requireOwner(tx.store, spaceId, userId, "inviter quelqu'un");
|
|
141
|
+
const existing = tx.store.findUserByEmail(email);
|
|
142
|
+
if (existing?.status === "disabled")
|
|
143
|
+
throw new ForbiddenError(`Le compte ${email} est désactivé : il ne peut pas être invité.`);
|
|
144
|
+
if (existing && tx.store.isMember(spaceId, existing.id))
|
|
145
|
+
throw new InvariantError(`${email} est déjà membre de « ${space.name} ».`);
|
|
146
|
+
if (existing?.status === "active") {
|
|
147
|
+
return { kind: "added", member: memberView(existing, tx.addMember(spaceId, existing.id)) };
|
|
148
|
+
}
|
|
149
|
+
if (tx.store.listOpenInvitations(spaceId).some((invitation) => invitation.email === email)) {
|
|
150
|
+
throw new InvariantError(`${email} a déjà une invitation en attente pour « ${space.name} » : renvoyez-la plutôt.`);
|
|
151
|
+
}
|
|
152
|
+
const invited = existing ?? tx.createUser({ email, status: "invited" });
|
|
153
|
+
const invitation = tx.createInvitation({ spaceId, email, expiresAt });
|
|
154
|
+
return { kind: "invited", space, invitation, issued: issueAccessLink(tx, invited.id, "invite", invitation.id, now) };
|
|
155
|
+
});
|
|
156
|
+
if (outcome.kind === "added")
|
|
157
|
+
return { outcome: "member_added", member: outcome.member };
|
|
158
|
+
const link = invitationLink(outcome.issued.token, outcome.issued.expiresAt);
|
|
159
|
+
const emailOutcome = await sendInvitationEmail(deps, email, store.getUser(userId), outcome.space, link);
|
|
160
|
+
return { outcome: "invited", invitation: invitationView(outcome.invitation, now), link, email: emailOutcome };
|
|
161
|
+
}
|
|
162
|
+
/** Invitation en attente d'un espace dont l'utilisateur est propriétaire. */
|
|
163
|
+
function ownedOpenInvitation(store, userId, spaceId, invitationId, action) {
|
|
164
|
+
const space = requireOwner(store, spaceId, userId, action);
|
|
165
|
+
const invitation = store.findInvitation(invitationId);
|
|
166
|
+
if (!invitation || invitation.spaceId !== spaceId || invitation.acceptedAt !== null || invitation.revokedAt !== null || invitation.declinedAt !== null) {
|
|
167
|
+
throw new NotFoundError("Cette invitation n'est plus en attente.");
|
|
168
|
+
}
|
|
169
|
+
return { space, invitation };
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Renvoie une invitation avec une nouvelle échéance. Un compte encore à
|
|
173
|
+
* activer reçoit un nouveau lien, qui remplace le lien précédent de cette
|
|
174
|
+
* invitation seulement ; un compte déjà actif retrouve l'invitation dans ses
|
|
175
|
+
* « Invitations reçues », sans lien.
|
|
176
|
+
*/
|
|
177
|
+
export async function resendInvitation(store, deps, userId, spaceId, invitationId) {
|
|
178
|
+
const now = deps.now?.() ?? new Date();
|
|
179
|
+
const expiresAt = new Date(now.getTime() + ACCESS_LINK_TTL_MS).toISOString();
|
|
180
|
+
const { space, renewed, issued } = store.mutate({ type: "user", userId }, (tx) => {
|
|
181
|
+
const { space, invitation } = ownedOpenInvitation(tx.store, userId, spaceId, invitationId, "renvoyer une invitation");
|
|
182
|
+
const invited = tx.store.findUserByEmail(invitation.email);
|
|
183
|
+
if (!invited || invited.status === "disabled")
|
|
184
|
+
throw new InvariantError(`Le compte ${invitation.email} est désactivé : révoquez l'invitation.`);
|
|
185
|
+
const renewed = tx.renewInvitation(invitation.id, expiresAt);
|
|
186
|
+
const issued = invited.status === "invited" ? issueAccessLink(tx, invited.id, "invite", invitation.id, now) : null;
|
|
187
|
+
return { space, renewed, issued };
|
|
188
|
+
});
|
|
189
|
+
if (issued === null) {
|
|
190
|
+
return {
|
|
191
|
+
invitation: invitationView(renewed, now),
|
|
192
|
+
link: null,
|
|
193
|
+
email: {
|
|
194
|
+
emailSent: false,
|
|
195
|
+
reason: `${renewed.email} a déjà un compte actif : aucun lien n'est nécessaire, l'invitation l'attend dans ses « Invitations reçues ».`,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const link = invitationLink(issued.token, issued.expiresAt);
|
|
200
|
+
const emailOutcome = await sendInvitationEmail(deps, renewed.email, store.getUser(userId), space, link);
|
|
201
|
+
return { invitation: invitationView(renewed, now), link, email: emailOutcome };
|
|
202
|
+
}
|
|
203
|
+
/** Révoque une invitation ; son lien d'activation ne fonctionne plus. */
|
|
204
|
+
export function revokeInvitation(store, userId, spaceId, invitationId) {
|
|
205
|
+
store.mutate({ type: "user", userId }, (tx) => {
|
|
206
|
+
const { invitation } = ownedOpenInvitation(tx.store, userId, spaceId, invitationId, "révoquer une invitation");
|
|
207
|
+
tx.revokeInvitation(invitation.id);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// --- Invitations reçues ---------------------------------------------------------
|
|
211
|
+
/** Invitation en attente adressée à l'utilisateur ; toute autre reçoit la même réponse qu'une invitation inexistante. */
|
|
212
|
+
function receivedInvitation(store, userId, invitationId) {
|
|
213
|
+
const user = store.getUser(userId);
|
|
214
|
+
const invitation = store.findInvitation(invitationId);
|
|
215
|
+
if (!invitation || invitation.email !== user.email || invitation.acceptedAt !== null || invitation.revokedAt !== null || invitation.declinedAt !== null) {
|
|
216
|
+
throw new NotFoundError("Invitation introuvable, ou elle n'est plus en attente.");
|
|
217
|
+
}
|
|
218
|
+
return invitation;
|
|
219
|
+
}
|
|
220
|
+
/** Invitations en attente et non expirées adressées à l'utilisateur, par date. */
|
|
221
|
+
export function listReceivedInvitations(store, userId, now = new Date()) {
|
|
222
|
+
const user = store.getUser(userId);
|
|
223
|
+
const invitations = store
|
|
224
|
+
.listOpenInvitationsForEmail(user.email)
|
|
225
|
+
.filter((invitation) => !isExpired(invitation, now))
|
|
226
|
+
.map((invitation) => ({
|
|
227
|
+
id: invitation.id,
|
|
228
|
+
spaceId: invitation.spaceId,
|
|
229
|
+
spaceName: store.getSpace(invitation.spaceId).name,
|
|
230
|
+
invitedByName: userName(store.findUser(invitation.invitedBy)),
|
|
231
|
+
createdAt: invitation.createdAt,
|
|
232
|
+
expiresAt: invitation.expiresAt,
|
|
233
|
+
}));
|
|
234
|
+
return { invitations };
|
|
235
|
+
}
|
|
236
|
+
/** L'utilisateur accepte une invitation qui lui est adressée et rejoint l'espace. */
|
|
237
|
+
export function acceptReceivedInvitation(store, userId, invitationId, now = new Date()) {
|
|
238
|
+
return store.mutate({ type: "user", userId }, (tx) => {
|
|
239
|
+
const invitation = receivedInvitation(tx.store, userId, invitationId);
|
|
240
|
+
if (isExpired(invitation, now)) {
|
|
241
|
+
throw new InvariantError(`Cette invitation a expiré : demandez à ${userName(tx.store.findUser(invitation.invitedBy))} de vous la renvoyer.`);
|
|
242
|
+
}
|
|
243
|
+
joinInvitedSpace(tx, userId, invitation.id);
|
|
244
|
+
return summary(tx.store.getSpace(invitation.spaceId), tx.store.findMembership(invitation.spaceId, userId));
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/** L'utilisateur refuse une invitation qui lui est adressée. */
|
|
248
|
+
export function declineReceivedInvitation(store, userId, invitationId) {
|
|
249
|
+
store.mutate({ type: "user", userId }, (tx) => {
|
|
250
|
+
tx.declineInvitation(receivedInvitation(tx.store, userId, invitationId).id);
|
|
251
|
+
});
|
|
252
|
+
}
|