alepha 0.20.1 → 0.20.2
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/dist/api/files/index.js +2 -1
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +64 -148
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +371 -573
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +605 -1012
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +78 -17
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +90 -23
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/payments/index.d.ts +2 -1
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js +4 -2
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/users/index.d.ts +34 -31
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +13 -7
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.js +2 -1
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +8 -34
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +43 -232
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +36 -11
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +93 -27
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/command/index.d.ts +1 -1
- package/dist/core/index.browser.js +6 -0
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +6 -0
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +6 -0
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/react/form/index.d.ts +60 -1
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +86 -1
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/head/index.browser.js +16 -1
- package/dist/react/head/index.browser.js.map +1 -1
- package/dist/react/head/index.d.ts +6 -0
- package/dist/react/head/index.d.ts.map +1 -1
- package/dist/react/head/index.js +16 -1
- package/dist/react/head/index.js.map +1 -1
- package/dist/react/router/index.browser.js +0 -10
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +35 -12
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +0 -10
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/ui/index.d.ts +124 -0
- package/dist/react/ui/index.d.ts.map +1 -0
- package/dist/react/ui/index.js +206 -0
- package/dist/react/ui/index.js.map +1 -0
- package/dist/router/index.d.ts +13 -13
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +45 -32
- package/dist/router/index.js.map +1 -1
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +1 -0
- package/dist/system/index.js.map +1 -1
- package/dist/topic/core/index.js +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/package.json +6 -23
- package/src/api/files/jobs/FileJobs.ts +2 -1
- package/src/api/jobs/__tests__/$job.spec.ts +316 -2867
- package/src/api/jobs/controllers/AdminJobController.ts +29 -138
- package/src/api/jobs/entities/jobExecutionEntity.ts +27 -19
- package/src/api/jobs/index.browser.ts +5 -7
- package/src/api/jobs/index.ts +23 -51
- package/src/api/jobs/primitives/$job.ts +66 -58
- package/src/api/jobs/providers/JobProvider.ts +561 -566
- package/src/api/jobs/providers/JobQueueProvider.ts +18 -19
- package/src/api/jobs/schemas/jobConfigAtom.ts +20 -23
- package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +3 -27
- package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +5 -7
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +7 -4
- package/src/api/jobs/schemas/triggerJobSchema.ts +0 -1
- package/src/api/jobs/services/JobService.ts +90 -483
- package/src/api/notifications/controllers/AdminNotificationController.ts +19 -12
- package/src/api/notifications/index.ts +7 -4
- package/src/api/notifications/jobs/NotificationJobs.ts +83 -12
- package/src/api/payments/services/PaymentService.ts +4 -2
- package/src/api/users/__tests__/UserJobs.spec.ts +10 -49
- package/src/api/users/audits/UserAudits.ts +3 -1
- package/src/api/users/buckets/UserBuckets.ts +2 -1
- package/src/api/users/index.ts +1 -4
- package/src/api/users/jobs/UserJobs.ts +5 -4
- package/src/api/verifications/jobs/VerificationJobs.ts +2 -1
- package/src/cli/core/__tests__/init.spec.ts +1 -1
- package/src/cli/core/commands/init.ts +0 -12
- package/src/cli/core/services/PackageManagerUtils.ts +2 -9
- package/src/cli/core/services/ProjectScaffolder.ts +17 -65
- package/src/cli/core/templates/agentMd.ts +2 -8
- package/src/cli/core/templates/apiIndexTs.ts +4 -18
- package/src/cli/core/templates/mainCss.ts +1 -36
- package/src/cli/core/templates/vitestConfigTs.ts +17 -0
- package/src/cli/core/templates/webAppRouterTs.ts +2 -85
- package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +22 -71
- package/src/cli/platform/adapters/CloudflareAdapter.ts +12 -11
- package/src/cli/platform/atoms/platformOptions.ts +9 -0
- package/src/cli/platform/schemas/cloudflare.ts +3 -2
- package/src/cli/platform/services/CloudflareApi.ts +164 -25
- package/src/cli/platform/services/WranglerApi.ts +0 -17
- package/src/core/Alepha.ts +9 -0
- package/src/react/form/index.ts +2 -0
- package/src/react/form/services/parseField.ts +163 -0
- package/src/react/form/services/prettyName.ts +19 -0
- package/src/react/head/providers/BrowserHeadProvider.ts +31 -10
- package/src/react/router/primitives/$page.ts +35 -12
- package/src/react/ui/atoms/uiAtom.ts +28 -0
- package/src/react/ui/components/ColorScheme.tsx +36 -0
- package/src/react/ui/hooks/useColorMode.ts +49 -0
- package/src/react/ui/hooks/useSidebarState.ts +26 -0
- package/src/react/ui/hooks/useTheme.ts +22 -0
- package/src/react/ui/index.ts +35 -0
- package/src/react/ui/services/UiPersistence.ts +41 -0
- package/src/router/TemplatedPathParser.ts +50 -51
- package/src/router/__tests__/RouterProvider.spec.ts +62 -0
- package/src/router/__tests__/TemplatedPathParser.spec.ts +18 -0
- package/src/router/providers/RouterProvider.ts +10 -5
- package/src/system/providers/NodeShellProvider.ts +1 -0
- package/src/topic/core/providers/TopicProvider.ts +1 -1
- package/dist/api/invitations/index.d.ts +0 -790
- package/dist/api/invitations/index.d.ts.map +0 -1
- package/dist/api/invitations/index.js +0 -662
- package/dist/api/invitations/index.js.map +0 -1
- package/dist/api/issues/index.d.ts +0 -810
- package/dist/api/issues/index.d.ts.map +0 -1
- package/dist/api/issues/index.js +0 -444
- package/dist/api/issues/index.js.map +0 -1
- package/dist/api/subscriptions/index.d.ts +0 -1692
- package/dist/api/subscriptions/index.d.ts.map +0 -1
- package/dist/api/subscriptions/index.js +0 -1867
- package/dist/api/subscriptions/index.js.map +0 -1
- package/dist/api/workflows/index.browser.js +0 -246
- package/dist/api/workflows/index.browser.js.map +0 -1
- package/dist/api/workflows/index.d.ts +0 -1618
- package/dist/api/workflows/index.d.ts.map +0 -1
- package/dist/api/workflows/index.js +0 -1495
- package/dist/api/workflows/index.js.map +0 -1
- package/src/api/invitations/__tests__/InvitationService.spec.ts +0 -439
- package/src/api/invitations/controllers/AdminInvitationController.ts +0 -86
- package/src/api/invitations/controllers/InvitationController.ts +0 -84
- package/src/api/invitations/entities/invitations.ts +0 -33
- package/src/api/invitations/index.ts +0 -58
- package/src/api/invitations/jobs/InvitationJobs.ts +0 -37
- package/src/api/invitations/providers/InvitationProvider.ts +0 -45
- package/src/api/invitations/schemas/createInvitationSchema.ts +0 -12
- package/src/api/invitations/schemas/invitationConfigAtom.ts +0 -20
- package/src/api/invitations/schemas/invitationQuerySchema.ts +0 -15
- package/src/api/invitations/schemas/invitationResourceSchema.ts +0 -6
- package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +0 -22
- package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +0 -10
- package/src/api/invitations/services/InvitationService.ts +0 -556
- package/src/api/issues/__tests__/IssueService.spec.ts +0 -263
- package/src/api/issues/controllers/AdminIssueController.ts +0 -149
- package/src/api/issues/controllers/IssueController.ts +0 -44
- package/src/api/issues/entities/issues.ts +0 -49
- package/src/api/issues/index.ts +0 -50
- package/src/api/issues/schemas/createIssueSchema.ts +0 -13
- package/src/api/issues/schemas/issueConfigAtom.ts +0 -13
- package/src/api/issues/schemas/issueQuerySchema.ts +0 -18
- package/src/api/issues/schemas/issueResourceSchema.ts +0 -6
- package/src/api/issues/schemas/myIssueQuerySchema.ts +0 -10
- package/src/api/issues/schemas/updateIssueSchema.ts +0 -13
- package/src/api/issues/services/IssueService.ts +0 -264
- package/src/api/jobs/__tests__/$job-middleware.spec.ts +0 -126
- package/src/api/jobs/__tests__/JobService.spec.ts +0 -31
- package/src/api/jobs/entities/jobExecutionLogEntity.ts +0 -13
- package/src/api/jobs/schemas/jobActivitySchema.ts +0 -15
- package/src/api/jobs/schemas/jobCronInfoSchema.ts +0 -22
- package/src/api/jobs/schemas/jobExecutionDetailResourceSchema.ts +0 -20
- package/src/api/jobs/schemas/jobFailureSchema.ts +0 -9
- package/src/api/jobs/schemas/jobQueueDepthSchema.ts +0 -14
- package/src/api/jobs/schemas/jobStatsSchema.ts +0 -14
- package/src/api/jobs/services/JobService-tests.ts +0 -157
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +0 -218
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +0 -278
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +0 -212
- package/src/api/subscriptions/controllers/SubscriptionController.ts +0 -189
- package/src/api/subscriptions/entities/subscriptionEvents.ts +0 -54
- package/src/api/subscriptions/entities/subscriptions.ts +0 -68
- package/src/api/subscriptions/index.ts +0 -133
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +0 -382
- package/src/api/subscriptions/middleware/$requireLimit.ts +0 -50
- package/src/api/subscriptions/middleware/$requirePlan.ts +0 -49
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +0 -110
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +0 -8
- package/src/api/subscriptions/schemas/changePlanSchema.ts +0 -9
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +0 -11
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +0 -21
- package/src/api/subscriptions/schemas/mrrSchema.ts +0 -13
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +0 -71
- package/src/api/subscriptions/schemas/planResourceSchema.ts +0 -25
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +0 -8
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +0 -19
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +0 -6
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +0 -32
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +0 -23
- package/src/api/subscriptions/services/BillingService.ts +0 -437
- package/src/api/subscriptions/services/SubscriptionConfig.ts +0 -56
- package/src/api/subscriptions/services/SubscriptionService.ts +0 -867
- package/src/api/subscriptions/services/UsageService.ts +0 -118
- package/src/api/workflows/__tests__/$workflow.spec.ts +0 -616
- package/src/api/workflows/controllers/AdminWorkflowController.ts +0 -191
- package/src/api/workflows/entities/workflowExecutions.ts +0 -74
- package/src/api/workflows/entities/workflowStepExecutions.ts +0 -74
- package/src/api/workflows/entities/workflowStepLogs.ts +0 -13
- package/src/api/workflows/index.browser.ts +0 -22
- package/src/api/workflows/index.ts +0 -115
- package/src/api/workflows/jobs/WorkflowJobs.ts +0 -77
- package/src/api/workflows/primitives/$workflow.ts +0 -202
- package/src/api/workflows/providers/WorkflowProvider.ts +0 -1284
- package/src/api/workflows/schemas/workflowActivitySchema.ts +0 -15
- package/src/api/workflows/schemas/workflowConfigAtom.ts +0 -51
- package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +0 -18
- package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +0 -26
- package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +0 -30
- package/src/api/workflows/schemas/workflowRegistrationSchema.ts +0 -26
- package/src/api/workflows/schemas/workflowStatsSchema.ts +0 -16
- package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +0 -15
- package/src/api/workflows/services/WorkflowService.ts +0 -382
- package/src/cli/core/templates/apiAppSecurityTs.ts +0 -43
- package/src/cli/core/templates/webAdminDashboardTsx.ts +0 -17
|
@@ -1,556 +0,0 @@
|
|
|
1
|
-
import { $inject, Alepha } from "alepha";
|
|
2
|
-
import { type UserEntity, users } from "alepha/api/users";
|
|
3
|
-
import { CryptoProvider } from "alepha/crypto";
|
|
4
|
-
import { DateTimeProvider } from "alepha/datetime";
|
|
5
|
-
import { $logger } from "alepha/logger";
|
|
6
|
-
import { $repository, type Page } from "alepha/orm";
|
|
7
|
-
import { BadRequestError, ForbiddenError } from "alepha/server";
|
|
8
|
-
import { type InvitationEntity, invitations } from "../entities/invitations.ts";
|
|
9
|
-
import { InvitationProvider } from "../providers/InvitationProvider.ts";
|
|
10
|
-
import type { CreateInvitation } from "../schemas/createInvitationSchema.ts";
|
|
11
|
-
import { invitationConfigAtom } from "../schemas/invitationConfigAtom.ts";
|
|
12
|
-
import type { InvitationQuery } from "../schemas/invitationQuerySchema.ts";
|
|
13
|
-
import type { InvitationWithResourceInfo } from "../schemas/invitationWithResourceInfoSchema.ts";
|
|
14
|
-
import type { MyInvitationsQuery } from "../schemas/myInvitationsQuerySchema.ts";
|
|
15
|
-
|
|
16
|
-
export class InvitationService {
|
|
17
|
-
protected readonly alepha = $inject(Alepha);
|
|
18
|
-
protected readonly log = $logger();
|
|
19
|
-
protected readonly repo = $repository(invitations);
|
|
20
|
-
protected readonly users = $repository(users);
|
|
21
|
-
protected readonly crypto = $inject(CryptoProvider);
|
|
22
|
-
protected readonly dateTime = $inject(DateTimeProvider);
|
|
23
|
-
protected readonly provider = $inject(InvitationProvider);
|
|
24
|
-
|
|
25
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Get an invitation by ID.
|
|
29
|
-
*/
|
|
30
|
-
public async getById(id: string): Promise<InvitationEntity> {
|
|
31
|
-
return this.repo.getById(id);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Create a new invitation.
|
|
38
|
-
*/
|
|
39
|
-
public async create(
|
|
40
|
-
data: CreateInvitation,
|
|
41
|
-
inviter: { id: string; email?: string },
|
|
42
|
-
): Promise<InvitationEntity> {
|
|
43
|
-
if (data.email === inviter.email) {
|
|
44
|
-
throw new BadRequestError("Cannot invite yourself");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
await this.provider.validateResource(
|
|
48
|
-
data.resourceType,
|
|
49
|
-
data.resourceId,
|
|
50
|
-
inviter,
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
const existingUser = await this.users.findOne({
|
|
54
|
-
where: { email: { eq: data.email } },
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
if (existingUser) {
|
|
58
|
-
const alreadyMember = await this.provider.isMember(
|
|
59
|
-
data.resourceType,
|
|
60
|
-
data.resourceId,
|
|
61
|
-
data.email,
|
|
62
|
-
existingUser.id,
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
if (alreadyMember) {
|
|
66
|
-
throw new BadRequestError("User is already a member of this resource");
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const pendingForSameTarget = await this.repo.findOne({
|
|
71
|
-
where: {
|
|
72
|
-
resourceType: { eq: data.resourceType },
|
|
73
|
-
resourceId: { eq: data.resourceId },
|
|
74
|
-
email: { eq: data.email },
|
|
75
|
-
status: { eq: "pending" },
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
if (pendingForSameTarget) {
|
|
80
|
-
throw new BadRequestError(
|
|
81
|
-
"A pending invitation already exists for this email and resource",
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const config = this.alepha.store.get(invitationConfigAtom);
|
|
86
|
-
|
|
87
|
-
const resourcePendingCount = await this.repo.count({
|
|
88
|
-
resourceType: { eq: data.resourceType },
|
|
89
|
-
resourceId: { eq: data.resourceId },
|
|
90
|
-
status: { eq: "pending" },
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
if (resourcePendingCount >= config.maxPendingPerResource) {
|
|
94
|
-
throw new BadRequestError(
|
|
95
|
-
`Maximum pending invitations per resource reached (${config.maxPendingPerResource})`,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const inviterPendingCount = await this.repo.count({
|
|
100
|
-
invitedBy: { eq: inviter.id },
|
|
101
|
-
status: { eq: "pending" },
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
if (inviterPendingCount >= config.maxPendingPerInviter) {
|
|
105
|
-
throw new BadRequestError(
|
|
106
|
-
`Maximum pending invitations per inviter reached (${config.maxPendingPerInviter})`,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const token = this.crypto.randomUUID();
|
|
111
|
-
const tokenHash = this.crypto.hash(token, "sha256");
|
|
112
|
-
const expiresAt = this.dateTime
|
|
113
|
-
.now()
|
|
114
|
-
.add(config.expirationDays, "days")
|
|
115
|
-
.toISOString();
|
|
116
|
-
|
|
117
|
-
const entity = await this.repo.create({
|
|
118
|
-
invitedBy: inviter.id,
|
|
119
|
-
email: data.email,
|
|
120
|
-
resourceType: data.resourceType,
|
|
121
|
-
resourceId: data.resourceId,
|
|
122
|
-
status: "pending",
|
|
123
|
-
roles: data.roles,
|
|
124
|
-
metadata: data.metadata,
|
|
125
|
-
token: tokenHash,
|
|
126
|
-
expiresAt,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
this.log.info("Invitation created", {
|
|
130
|
-
id: entity.id,
|
|
131
|
-
email: data.email,
|
|
132
|
-
resourceType: data.resourceType,
|
|
133
|
-
resourceId: data.resourceId,
|
|
134
|
-
invitedBy: inviter.id,
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
await this.alepha.events.emit("invitation:created", {
|
|
138
|
-
invitation: entity,
|
|
139
|
-
token,
|
|
140
|
-
inviter,
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
return entity;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Accept a pending invitation.
|
|
150
|
-
*/
|
|
151
|
-
public async accept(
|
|
152
|
-
invitationId: string,
|
|
153
|
-
acceptedBy: { id: string; email?: string },
|
|
154
|
-
): Promise<void> {
|
|
155
|
-
const invitation = await this.repo.getById(invitationId);
|
|
156
|
-
|
|
157
|
-
if (invitation.status !== "pending") {
|
|
158
|
-
throw new BadRequestError(
|
|
159
|
-
`Invitation is not pending (current status: ${invitation.status})`,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (invitation.email !== acceptedBy.email) {
|
|
164
|
-
throw new ForbiddenError("This invitation was sent to a different email");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const now = this.dateTime.now();
|
|
168
|
-
|
|
169
|
-
if (now.isAfter(invitation.expiresAt)) {
|
|
170
|
-
await this.repo.updateById(invitationId, {
|
|
171
|
-
status: "expired",
|
|
172
|
-
resolvedAt: now.toISOString(),
|
|
173
|
-
});
|
|
174
|
-
throw new BadRequestError("Invitation has expired");
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const alreadyMember = await this.provider.isMember(
|
|
178
|
-
invitation.resourceType,
|
|
179
|
-
invitation.resourceId,
|
|
180
|
-
invitation.email,
|
|
181
|
-
acceptedBy.id,
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
if (alreadyMember) {
|
|
185
|
-
await this.repo.updateById(invitationId, {
|
|
186
|
-
status: "accepted",
|
|
187
|
-
resolvedAt: now.toISOString(),
|
|
188
|
-
resolvedBy: acceptedBy.id,
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
this.log.info("Invitation accepted (already member)", {
|
|
192
|
-
id: invitationId,
|
|
193
|
-
acceptedBy: acceptedBy.id,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
await this.provider.onAccept(invitation, acceptedBy);
|
|
200
|
-
|
|
201
|
-
await this.repo.updateById(invitationId, {
|
|
202
|
-
status: "accepted",
|
|
203
|
-
resolvedAt: now.toISOString(),
|
|
204
|
-
resolvedBy: acceptedBy.id,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
this.log.info("Invitation accepted", {
|
|
208
|
-
id: invitationId,
|
|
209
|
-
email: invitation.email,
|
|
210
|
-
resourceType: invitation.resourceType,
|
|
211
|
-
resourceId: invitation.resourceId,
|
|
212
|
-
acceptedBy: acceptedBy.id,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
await this.alepha.events.emit("invitation:accepted", {
|
|
216
|
-
invitation,
|
|
217
|
-
acceptedBy,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Decline a pending invitation.
|
|
225
|
-
*/
|
|
226
|
-
public async decline(
|
|
227
|
-
invitationId: string,
|
|
228
|
-
declinedBy: { id: string; email?: string },
|
|
229
|
-
): Promise<void> {
|
|
230
|
-
const invitation = await this.repo.getById(invitationId);
|
|
231
|
-
|
|
232
|
-
if (invitation.status !== "pending") {
|
|
233
|
-
throw new BadRequestError(
|
|
234
|
-
`Invitation is not pending (current status: ${invitation.status})`,
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (invitation.email !== declinedBy.email) {
|
|
239
|
-
throw new ForbiddenError("This invitation was sent to a different email");
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const now = this.dateTime.now();
|
|
243
|
-
|
|
244
|
-
await this.repo.updateById(invitationId, {
|
|
245
|
-
status: "declined",
|
|
246
|
-
resolvedAt: now.toISOString(),
|
|
247
|
-
resolvedBy: declinedBy.id,
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
this.log.info("Invitation declined", {
|
|
251
|
-
id: invitationId,
|
|
252
|
-
email: invitation.email,
|
|
253
|
-
resourceType: invitation.resourceType,
|
|
254
|
-
resourceId: invitation.resourceId,
|
|
255
|
-
declinedBy: declinedBy.id,
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
await this.alepha.events.emit("invitation:declined", {
|
|
259
|
-
invitation,
|
|
260
|
-
declinedBy,
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Revoke a pending invitation (by the inviter or admin).
|
|
268
|
-
*/
|
|
269
|
-
public async revoke(
|
|
270
|
-
invitationId: string,
|
|
271
|
-
revokedBy: { id: string },
|
|
272
|
-
): Promise<void> {
|
|
273
|
-
const invitation = await this.repo.getById(invitationId);
|
|
274
|
-
|
|
275
|
-
if (invitation.status !== "pending") {
|
|
276
|
-
throw new BadRequestError(
|
|
277
|
-
`Invitation is not pending (current status: ${invitation.status})`,
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const now = this.dateTime.now();
|
|
282
|
-
|
|
283
|
-
await this.repo.updateById(invitationId, {
|
|
284
|
-
status: "revoked",
|
|
285
|
-
resolvedAt: now.toISOString(),
|
|
286
|
-
resolvedBy: revokedBy.id,
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
this.log.info("Invitation revoked", {
|
|
290
|
-
id: invitationId,
|
|
291
|
-
email: invitation.email,
|
|
292
|
-
resourceType: invitation.resourceType,
|
|
293
|
-
resourceId: invitation.resourceId,
|
|
294
|
-
revokedBy: revokedBy.id,
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
await this.alepha.events.emit("invitation:revoked", {
|
|
298
|
-
invitation,
|
|
299
|
-
revokedBy,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Find invitations for a given email with resource info enrichment.
|
|
307
|
-
*/
|
|
308
|
-
public async findByEmail(
|
|
309
|
-
email: string,
|
|
310
|
-
query: MyInvitationsQuery = {},
|
|
311
|
-
): Promise<InvitationWithResourceInfo[]> {
|
|
312
|
-
const where = this.repo.createQueryWhere();
|
|
313
|
-
where.email = { eq: email };
|
|
314
|
-
|
|
315
|
-
if (query.status) {
|
|
316
|
-
where.status = { eq: query.status };
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const results = await this.repo.findMany({
|
|
320
|
-
where,
|
|
321
|
-
orderBy: { column: "createdAt", direction: "desc" },
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
const inviterIds = [...new Set(results.map((inv) => inv.invitedBy))];
|
|
325
|
-
const inviters = await this.loadInviters(inviterIds);
|
|
326
|
-
|
|
327
|
-
const enriched: InvitationWithResourceInfo[] = [];
|
|
328
|
-
|
|
329
|
-
for (const inv of results) {
|
|
330
|
-
const inviter = inviters.get(inv.invitedBy);
|
|
331
|
-
let resourceName = inv.resourceType;
|
|
332
|
-
let resourceUrl: string | undefined;
|
|
333
|
-
|
|
334
|
-
try {
|
|
335
|
-
const info = await this.provider.getResourceInfo(
|
|
336
|
-
inv.resourceType,
|
|
337
|
-
inv.resourceId,
|
|
338
|
-
);
|
|
339
|
-
resourceName = info.name;
|
|
340
|
-
resourceUrl = info.url;
|
|
341
|
-
} catch (error) {
|
|
342
|
-
this.log.warn("Failed to load resource info for invitation", {
|
|
343
|
-
invitationId: inv.id,
|
|
344
|
-
resourceType: inv.resourceType,
|
|
345
|
-
resourceId: inv.resourceId,
|
|
346
|
-
error,
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
enriched.push({
|
|
351
|
-
id: inv.id,
|
|
352
|
-
email: inv.email,
|
|
353
|
-
resourceType: inv.resourceType,
|
|
354
|
-
resourceId: inv.resourceId,
|
|
355
|
-
resourceName,
|
|
356
|
-
resourceUrl,
|
|
357
|
-
invitedBy: inv.invitedBy,
|
|
358
|
-
inviterName: this.formatInviterName(inviter),
|
|
359
|
-
inviterEmail: inviter?.email,
|
|
360
|
-
roles: inv.roles,
|
|
361
|
-
status: inv.status,
|
|
362
|
-
createdAt: inv.createdAt,
|
|
363
|
-
expiresAt: inv.expiresAt,
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return enriched;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Find invitations for a specific resource.
|
|
374
|
-
*/
|
|
375
|
-
public async findByResource(
|
|
376
|
-
resourceType: string,
|
|
377
|
-
resourceId: string,
|
|
378
|
-
status?: string,
|
|
379
|
-
): Promise<InvitationEntity[]> {
|
|
380
|
-
const where = this.repo.createQueryWhere();
|
|
381
|
-
where.resourceType = { eq: resourceType };
|
|
382
|
-
where.resourceId = { eq: resourceId };
|
|
383
|
-
|
|
384
|
-
if (status) {
|
|
385
|
-
where.status = { eq: status as InvitationEntity["status"] };
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return this.repo.findMany({
|
|
389
|
-
where,
|
|
390
|
-
orderBy: { column: "createdAt", direction: "desc" },
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Find invitations with pagination and filtering (admin).
|
|
398
|
-
*/
|
|
399
|
-
public async findInvitations(
|
|
400
|
-
query: InvitationQuery = {},
|
|
401
|
-
): Promise<Page<InvitationEntity>> {
|
|
402
|
-
query.sort ??= "-createdAt";
|
|
403
|
-
|
|
404
|
-
const where = this.repo.createQueryWhere();
|
|
405
|
-
|
|
406
|
-
if (query.email) {
|
|
407
|
-
where.email = { like: `%${query.email}%` };
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (query.resourceType) {
|
|
411
|
-
where.resourceType = { eq: query.resourceType };
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (query.resourceId) {
|
|
415
|
-
where.resourceId = { eq: query.resourceId };
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (query.status) {
|
|
419
|
-
where.status = { eq: query.status };
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if (query.invitedBy) {
|
|
423
|
-
where.invitedBy = { eq: query.invitedBy };
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return this.repo.paginate(query, { where }, { count: true });
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Delete an invitation (admin). Only non-pending invitations can be deleted.
|
|
433
|
-
*/
|
|
434
|
-
public async deleteInvitation(id: string): Promise<void> {
|
|
435
|
-
const invitation = await this.repo.getById(id);
|
|
436
|
-
|
|
437
|
-
if (invitation.status === "pending") {
|
|
438
|
-
throw new BadRequestError(
|
|
439
|
-
"Cannot delete a pending invitation. Revoke it first.",
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
await this.repo.deleteById(id);
|
|
444
|
-
|
|
445
|
-
this.log.info("Invitation deleted", { id });
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Expire all pending invitations that have passed their expiration date.
|
|
452
|
-
* Returns the number of expired invitations.
|
|
453
|
-
*/
|
|
454
|
-
public async expirePending(): Promise<number> {
|
|
455
|
-
const now = this.dateTime.nowISOString();
|
|
456
|
-
|
|
457
|
-
const expired = await this.repo.findMany({
|
|
458
|
-
where: {
|
|
459
|
-
status: { eq: "pending" },
|
|
460
|
-
expiresAt: { lt: now },
|
|
461
|
-
},
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
if (expired.length === 0) {
|
|
465
|
-
return 0;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const ids = expired.map((inv) => inv.id);
|
|
469
|
-
|
|
470
|
-
await this.repo.updateMany(
|
|
471
|
-
{ id: { inArray: ids } },
|
|
472
|
-
{
|
|
473
|
-
status: "expired",
|
|
474
|
-
resolvedAt: now,
|
|
475
|
-
},
|
|
476
|
-
);
|
|
477
|
-
|
|
478
|
-
for (const inv of expired) {
|
|
479
|
-
await this.alepha.events.emit("invitation:expired", {
|
|
480
|
-
invitation: inv,
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
this.log.info("Expired pending invitations", { count: expired.length });
|
|
485
|
-
|
|
486
|
-
return expired.length;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Purge resolved invitations older than the configured purge days.
|
|
493
|
-
* Returns the number of purged invitations.
|
|
494
|
-
*/
|
|
495
|
-
public async purgeResolved(): Promise<number> {
|
|
496
|
-
const config = this.alepha.store.get(invitationConfigAtom);
|
|
497
|
-
|
|
498
|
-
if (config.purgeDays === 0) {
|
|
499
|
-
return 0;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const cutoff = this.dateTime
|
|
503
|
-
.now()
|
|
504
|
-
.subtract(config.purgeDays, "days")
|
|
505
|
-
.toISOString();
|
|
506
|
-
|
|
507
|
-
const ids = await this.repo.deleteMany({
|
|
508
|
-
status: { inArray: ["accepted", "declined", "expired", "revoked"] },
|
|
509
|
-
resolvedAt: { lt: cutoff },
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
if (ids.length > 0) {
|
|
513
|
-
this.log.info("Purged resolved invitations", { count: ids.length });
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
return ids.length;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Load user records for a list of inviter IDs.
|
|
523
|
-
*/
|
|
524
|
-
protected async loadInviters(
|
|
525
|
-
ids: string[],
|
|
526
|
-
): Promise<Map<string, UserEntity>> {
|
|
527
|
-
if (ids.length === 0) {
|
|
528
|
-
return new Map();
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const result = await this.users.findMany({
|
|
532
|
-
where: { id: { inArray: ids } },
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
return new Map(result.map((user) => [user.id, user]));
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Format inviter display name from user entity.
|
|
540
|
-
*/
|
|
541
|
-
protected formatInviterName(user?: UserEntity): string | undefined {
|
|
542
|
-
if (!user) {
|
|
543
|
-
return undefined;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
if (user.firstName && user.lastName) {
|
|
547
|
-
return `${user.firstName} ${user.lastName}`;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (user.firstName) {
|
|
551
|
-
return user.firstName;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
return user.username ?? user.email;
|
|
555
|
-
}
|
|
556
|
-
}
|