@m5kdev/backend 0.1.0
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/LICENSE +621 -0
- package/README.md +22 -0
- package/package.json +205 -0
- package/src/lib/posthog.ts +5 -0
- package/src/lib/sentry.ts +8 -0
- package/src/modules/access/access.repository.ts +36 -0
- package/src/modules/access/access.service.ts +81 -0
- package/src/modules/access/access.test.ts +216 -0
- package/src/modules/access/access.utils.ts +46 -0
- package/src/modules/ai/ai.db.ts +38 -0
- package/src/modules/ai/ai.prompt.ts +47 -0
- package/src/modules/ai/ai.repository.ts +53 -0
- package/src/modules/ai/ai.router.ts +148 -0
- package/src/modules/ai/ai.service.ts +310 -0
- package/src/modules/ai/ai.trpc.ts +22 -0
- package/src/modules/ai/ideogram/ideogram.constants.ts +170 -0
- package/src/modules/ai/ideogram/ideogram.dto.ts +64 -0
- package/src/modules/ai/ideogram/ideogram.prompt.ts +858 -0
- package/src/modules/ai/ideogram/ideogram.repository.ts +39 -0
- package/src/modules/ai/ideogram/ideogram.service.ts +14 -0
- package/src/modules/auth/auth.db.ts +224 -0
- package/src/modules/auth/auth.dto.ts +47 -0
- package/src/modules/auth/auth.lib.ts +349 -0
- package/src/modules/auth/auth.middleware.ts +62 -0
- package/src/modules/auth/auth.repository.ts +672 -0
- package/src/modules/auth/auth.service.ts +261 -0
- package/src/modules/auth/auth.trpc.ts +208 -0
- package/src/modules/auth/auth.utils.ts +117 -0
- package/src/modules/base/base.abstract.ts +62 -0
- package/src/modules/base/base.dto.ts +206 -0
- package/src/modules/base/base.grants.test.ts +861 -0
- package/src/modules/base/base.grants.ts +199 -0
- package/src/modules/base/base.repository.ts +433 -0
- package/src/modules/base/base.service.ts +154 -0
- package/src/modules/base/base.types.ts +7 -0
- package/src/modules/billing/billing.db.ts +27 -0
- package/src/modules/billing/billing.repository.ts +328 -0
- package/src/modules/billing/billing.router.ts +77 -0
- package/src/modules/billing/billing.service.ts +177 -0
- package/src/modules/billing/billing.trpc.ts +17 -0
- package/src/modules/clay/clay.repository.ts +29 -0
- package/src/modules/clay/clay.service.ts +61 -0
- package/src/modules/connect/connect.db.ts +32 -0
- package/src/modules/connect/connect.dto.ts +44 -0
- package/src/modules/connect/connect.linkedin.ts +70 -0
- package/src/modules/connect/connect.oauth.ts +288 -0
- package/src/modules/connect/connect.repository.ts +65 -0
- package/src/modules/connect/connect.router.ts +76 -0
- package/src/modules/connect/connect.service.ts +171 -0
- package/src/modules/connect/connect.trpc.ts +26 -0
- package/src/modules/connect/connect.types.ts +27 -0
- package/src/modules/crypto/crypto.db.ts +15 -0
- package/src/modules/crypto/crypto.repository.ts +13 -0
- package/src/modules/crypto/crypto.service.ts +57 -0
- package/src/modules/email/email.service.ts +222 -0
- package/src/modules/file/file.repository.ts +95 -0
- package/src/modules/file/file.router.ts +108 -0
- package/src/modules/file/file.service.ts +186 -0
- package/src/modules/recurrence/recurrence.db.ts +79 -0
- package/src/modules/recurrence/recurrence.repository.ts +70 -0
- package/src/modules/recurrence/recurrence.service.ts +105 -0
- package/src/modules/recurrence/recurrence.trpc.ts +82 -0
- package/src/modules/social/social.dto.ts +22 -0
- package/src/modules/social/social.linkedin.test.ts +277 -0
- package/src/modules/social/social.linkedin.ts +593 -0
- package/src/modules/social/social.service.ts +112 -0
- package/src/modules/social/social.types.ts +43 -0
- package/src/modules/tag/tag.db.ts +41 -0
- package/src/modules/tag/tag.dto.ts +18 -0
- package/src/modules/tag/tag.repository.ts +222 -0
- package/src/modules/tag/tag.service.ts +48 -0
- package/src/modules/tag/tag.trpc.ts +62 -0
- package/src/modules/uploads/0581796b-8845-420d-bd95-cd7de79f6d37.webm +0 -0
- package/src/modules/uploads/33b1e649-6727-4bd0-94d0-a0b363646865.webm +0 -0
- package/src/modules/uploads/49a8c4c0-54d7-4c94-bef4-c93c029f9ed0.webm +0 -0
- package/src/modules/uploads/50e31e38-a2f0-47ca-8b7d-2d7fcad9267d.webm +0 -0
- package/src/modules/uploads/72ac8cf9-c3a7-4cd8-8a78-6d8e137a4c7e.webm +0 -0
- package/src/modules/uploads/75293649-d966-46cd-a675-67518958ae9c.png +0 -0
- package/src/modules/uploads/88b7b867-ce15-4891-bf73-81305a7de1f7.wav +0 -0
- package/src/modules/uploads/a5d6fee8-6a59-42c6-9d4a-ac8a3c5e7245.webm +0 -0
- package/src/modules/uploads/c13a9785-ca5a-4983-af30-b338ed76d370.webm +0 -0
- package/src/modules/uploads/caa1a5a7-71ba-4381-902d-7e2cafdf6dcb.webm +0 -0
- package/src/modules/uploads/cbeb0b81-374d-445b-914b-40ace7c8e031.webm +0 -0
- package/src/modules/uploads/d626aa82-b10f-493f-aee7-87bfb3361dfc.webm +0 -0
- package/src/modules/uploads/d7de4c16-de0c-495d-9612-e72260a6ecca.png +0 -0
- package/src/modules/uploads/e532e38a-6421-400e-8a5f-8e7bc8ce411b.wav +0 -0
- package/src/modules/uploads/e86ec867-6adf-4c51-84e0-00b0836625e8.webm +0 -0
- package/src/modules/utils/applyPagination.ts +13 -0
- package/src/modules/utils/applySorting.ts +21 -0
- package/src/modules/utils/getConditionsFromFilters.ts +216 -0
- package/src/modules/video/video.service.ts +89 -0
- package/src/modules/webhook/webhook.constants.ts +9 -0
- package/src/modules/webhook/webhook.db.ts +15 -0
- package/src/modules/webhook/webhook.dto.ts +9 -0
- package/src/modules/webhook/webhook.repository.ts +68 -0
- package/src/modules/webhook/webhook.router.ts +29 -0
- package/src/modules/webhook/webhook.service.ts +78 -0
- package/src/modules/workflow/workflow.db.ts +29 -0
- package/src/modules/workflow/workflow.repository.ts +171 -0
- package/src/modules/workflow/workflow.service.ts +56 -0
- package/src/modules/workflow/workflow.trpc.ts +26 -0
- package/src/modules/workflow/workflow.types.ts +30 -0
- package/src/modules/workflow/workflow.utils.ts +259 -0
- package/src/test/stubs/utils.ts +2 -0
- package/src/trpc/context.ts +21 -0
- package/src/trpc/index.ts +3 -0
- package/src/trpc/procedures.ts +43 -0
- package/src/trpc/utils.ts +20 -0
- package/src/types.ts +22 -0
- package/src/utils/errors.ts +148 -0
- package/src/utils/logger.ts +8 -0
- package/src/utils/posthog.ts +43 -0
- package/src/utils/types.ts +5 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { err, ok } from "neverthrow";
|
|
2
|
+
import type {
|
|
3
|
+
AccountClaim,
|
|
4
|
+
AccountClaimMagicLinkOutput,
|
|
5
|
+
AccountClaimOutput,
|
|
6
|
+
Waitlist,
|
|
7
|
+
WaitlistOutput,
|
|
8
|
+
} from "#modules/auth/auth.dto";
|
|
9
|
+
import type { User } from "#modules/auth/auth.lib";
|
|
10
|
+
import type { AuthRepository } from "#modules/auth/auth.repository";
|
|
11
|
+
import type { ServerResultAsync } from "#modules/base/base.dto";
|
|
12
|
+
import { BaseService } from "#modules/base/base.service";
|
|
13
|
+
import type { BillingService } from "#modules/billing/billing.service";
|
|
14
|
+
import type { EmailService } from "#modules/email/email.service";
|
|
15
|
+
import { posthogCapture } from "#utils/posthog";
|
|
16
|
+
|
|
17
|
+
type AuthServiceDependencies =
|
|
18
|
+
| { email: EmailService }
|
|
19
|
+
| { email: EmailService; billing: BillingService };
|
|
20
|
+
|
|
21
|
+
export class AuthService extends BaseService<{ auth: AuthRepository }, AuthServiceDependencies> {
|
|
22
|
+
private getBillingService(): BillingService | null {
|
|
23
|
+
if (!("billing" in this.service)) return null;
|
|
24
|
+
return this.service.billing;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async getUserWaitlistCount({ user }: { user: User }): ServerResultAsync<number> {
|
|
28
|
+
if (user.role === "admin") return ok(0);
|
|
29
|
+
return this.repository.auth.getUserWaitlistCount(user.id);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async getOnboarding({ user }: { user: User }): ServerResultAsync<number> {
|
|
33
|
+
return this.repository.auth.getOnboarding(user.id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async setOnboarding(onboarding: number, { user }: { user: User }): ServerResultAsync<number> {
|
|
37
|
+
posthogCapture({
|
|
38
|
+
distinctId: user.id,
|
|
39
|
+
event: "onboarding_set",
|
|
40
|
+
properties: {
|
|
41
|
+
onboarding,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return this.repository.auth.setOnboarding(user.id, onboarding);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getPreferences({ user }: { user: User }): ServerResultAsync<Record<string, unknown>> {
|
|
48
|
+
return this.repository.auth.getPreferences(user.id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async setPreferences(
|
|
52
|
+
preferences: Record<string, unknown>,
|
|
53
|
+
{ user }: { user: User }
|
|
54
|
+
): ServerResultAsync<Record<string, unknown>> {
|
|
55
|
+
posthogCapture({
|
|
56
|
+
distinctId: user.id,
|
|
57
|
+
event: "preferences_set",
|
|
58
|
+
});
|
|
59
|
+
return this.repository.auth.setPreferences(user.id, preferences);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getMetadata({ user }: { user: User }): ServerResultAsync<Record<string, unknown>> {
|
|
63
|
+
return this.repository.auth.getMetadata(user.id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async setMetadata(
|
|
67
|
+
metadata: Record<string, unknown>,
|
|
68
|
+
{ user }: { user: User }
|
|
69
|
+
): ServerResultAsync<Record<string, unknown>> {
|
|
70
|
+
posthogCapture({
|
|
71
|
+
distinctId: user.id,
|
|
72
|
+
event: "metadata_set",
|
|
73
|
+
});
|
|
74
|
+
return this.repository.auth.setMetadata(user.id, metadata);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async getFlags({ user }: { user: User }): ServerResultAsync<string[]> {
|
|
78
|
+
return this.repository.auth.getFlags(user.id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async setFlags(flags: string[], { user }: { user: User }): ServerResultAsync<string[]> {
|
|
82
|
+
posthogCapture({
|
|
83
|
+
distinctId: user.id,
|
|
84
|
+
event: "flags_set",
|
|
85
|
+
});
|
|
86
|
+
return this.repository.auth.setFlags(user.id, flags);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async listAdminWaitlist(): ServerResultAsync<WaitlistOutput[]> {
|
|
90
|
+
return this.repository.auth.listAdminWaitlist();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async listWaitlist({ user }: { user: User }): ServerResultAsync<Waitlist[]> {
|
|
94
|
+
return this.repository.auth.listWaitlist(user.id);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async addToWaitlist({ email }: { email: string }): ServerResultAsync<WaitlistOutput> {
|
|
98
|
+
return this.repository.auth.addToWaitlist(email);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async inviteFromWaitlist({ id }: { id: string }): ServerResultAsync<Waitlist> {
|
|
102
|
+
const waitlist = await this.repository.auth.inviteFromWaitlist(id);
|
|
103
|
+
if (waitlist.isErr()) return err(waitlist.error);
|
|
104
|
+
if (!waitlist.value.code) return this.repository.auth.error("BAD_REQUEST");
|
|
105
|
+
if (!waitlist.value.email) return this.repository.auth.error("BAD_REQUEST");
|
|
106
|
+
await this.service.email.sendWaitlistInvite(waitlist.value.email, waitlist.value.code);
|
|
107
|
+
return ok(waitlist.value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async inviteToWaitlist(
|
|
111
|
+
{ email, name }: { email: string; name?: string },
|
|
112
|
+
{ user }: { user: User }
|
|
113
|
+
): ServerResultAsync<Waitlist> {
|
|
114
|
+
const count = await this.repository.auth.getUserWaitlistCount(user.id);
|
|
115
|
+
if (count.isErr()) return err(count.error);
|
|
116
|
+
if (count.value >= 3) return this.repository.auth.error("BAD_REQUEST", "Run out of invites");
|
|
117
|
+
const waitlist = await this.repository.auth.inviteToWaitlist({ email, userId: user.id, name });
|
|
118
|
+
if (waitlist.isErr()) return err(waitlist.error);
|
|
119
|
+
if (!waitlist.value.code) return this.repository.auth.error("BAD_REQUEST");
|
|
120
|
+
await this.service.email.sendWaitlistUserInvite(email, waitlist.value.code, user.name, name);
|
|
121
|
+
posthogCapture({
|
|
122
|
+
distinctId: user.id,
|
|
123
|
+
event: "waitlist_invite_sent",
|
|
124
|
+
properties: {
|
|
125
|
+
email,
|
|
126
|
+
name,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
return ok(waitlist.value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async createInvitationCode(
|
|
133
|
+
{ name }: { name?: string },
|
|
134
|
+
{ user }: { user: User }
|
|
135
|
+
): ServerResultAsync<Waitlist> {
|
|
136
|
+
posthogCapture({
|
|
137
|
+
distinctId: user.id,
|
|
138
|
+
event: "waitlist_invitation_code_created",
|
|
139
|
+
properties: {
|
|
140
|
+
name,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
return this.repository.auth.createInvitationCode({ userId: user.id, name });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async joinWaitlist({ email }: { email: string }): ServerResultAsync<WaitlistOutput> {
|
|
147
|
+
const waitlist = await this.repository.auth.joinWaitlist(email);
|
|
148
|
+
if (waitlist.isErr()) return err(waitlist.error);
|
|
149
|
+
await this.service.email.sendWaitlistConfirmation(email);
|
|
150
|
+
return ok(waitlist.value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async removeFromWaitlist({ id }: { id: string }): ServerResultAsync<WaitlistOutput> {
|
|
154
|
+
return this.repository.auth.removeFromWaitlist(id);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async validateWaitlistCode(code: string): ServerResultAsync<{ status: string }> {
|
|
158
|
+
return this.repository.auth.validateWaitlistCode(code);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async createAccountClaimCode({
|
|
162
|
+
userId,
|
|
163
|
+
expiresInHours,
|
|
164
|
+
}: {
|
|
165
|
+
userId: string;
|
|
166
|
+
expiresInHours?: number;
|
|
167
|
+
}): ServerResultAsync<AccountClaim> {
|
|
168
|
+
return this.repository.auth.createAccountClaimCode({ userId, expiresInHours });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async listAccountClaims(): ServerResultAsync<AccountClaimOutput[]> {
|
|
172
|
+
return this.repository.auth.listAccountClaims();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async getMyAccountClaimStatus({ user }: { user: User }): ServerResultAsync<AccountClaim | null> {
|
|
176
|
+
return this.repository.auth.findPendingAccountClaimForUser(user.id);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async setMyAccountClaimEmail(
|
|
180
|
+
{ email }: { email: string },
|
|
181
|
+
{ user }: { user: User }
|
|
182
|
+
): ServerResultAsync<{ status: boolean }> {
|
|
183
|
+
return this.repository.auth.setAccountClaimEmail({ userId: user.id, email });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async acceptMyAccountClaim({ user }: { user: User }): ServerResultAsync<{ status: boolean }> {
|
|
187
|
+
const pendingClaim = await this.repository.auth.findPendingAccountClaimForUser(user.id);
|
|
188
|
+
if (pendingClaim.isErr()) return err(pendingClaim.error);
|
|
189
|
+
|
|
190
|
+
const accepted = await this.repository.auth.acceptAccountClaim(user.id);
|
|
191
|
+
if (accepted.isErr()) return err(accepted.error);
|
|
192
|
+
|
|
193
|
+
if (pendingClaim.value) {
|
|
194
|
+
const billingService = this.getBillingService();
|
|
195
|
+
if (billingService) {
|
|
196
|
+
await billingService.createUserHook({ user });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return ok(accepted.value);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async generateAccountClaimMagicLink({
|
|
204
|
+
claimId,
|
|
205
|
+
email,
|
|
206
|
+
}: {
|
|
207
|
+
claimId: string;
|
|
208
|
+
email?: string;
|
|
209
|
+
}): ServerResultAsync<AccountClaimMagicLinkOutput> {
|
|
210
|
+
const claim = await this.repository.auth.findAccountClaimById(claimId);
|
|
211
|
+
if (claim.isErr()) return err(claim.error);
|
|
212
|
+
if (!claim.value) return this.repository.auth.error("NOT_FOUND", "Claim not found");
|
|
213
|
+
if (!claim.value.claimUserId)
|
|
214
|
+
return this.repository.auth.error("BAD_REQUEST", "Claim has no user");
|
|
215
|
+
if (claim.value.status !== "INVITED") {
|
|
216
|
+
return this.repository.auth.error("BAD_REQUEST", "Claim is not pending");
|
|
217
|
+
}
|
|
218
|
+
if (claim.value.expiresAt && claim.value.expiresAt < new Date()) {
|
|
219
|
+
return this.repository.auth.error("BAD_REQUEST", "Claim is expired");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const targetEmail = email ?? claim.value.claimedEmail ?? undefined;
|
|
223
|
+
if (!targetEmail) {
|
|
224
|
+
return this.repository.auth.error("BAD_REQUEST", "Email required to generate magic link");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const setEmail = await this.repository.auth.setAccountClaimEmail({
|
|
228
|
+
userId: claim.value.claimUserId,
|
|
229
|
+
email: targetEmail,
|
|
230
|
+
});
|
|
231
|
+
if (setEmail.isErr()) return err(setEmail.error);
|
|
232
|
+
|
|
233
|
+
const response = await fetch(`${process.env.VITE_SERVER_URL}/api/auth/sign-in/magic-link`, {
|
|
234
|
+
method: "POST",
|
|
235
|
+
headers: {
|
|
236
|
+
"content-type": "application/json",
|
|
237
|
+
},
|
|
238
|
+
body: JSON.stringify({
|
|
239
|
+
email: targetEmail.toLowerCase(),
|
|
240
|
+
callbackURL: `${process.env.VITE_APP_URL}/claim-account?claim=${claimId}`,
|
|
241
|
+
}),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
return this.repository.auth.error("INTERNAL_SERVER_ERROR", "Failed to generate magic link");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const latest = await this.repository.auth.latestAccountClaimMagicLink(claimId);
|
|
249
|
+
if (latest.isErr()) return err(latest.error);
|
|
250
|
+
if (!latest.value) return this.repository.auth.error("INTERNAL_SERVER_ERROR");
|
|
251
|
+
return ok(latest.value);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async listAccountClaimMagicLinks({
|
|
255
|
+
claimId,
|
|
256
|
+
}: {
|
|
257
|
+
claimId: string;
|
|
258
|
+
}): ServerResultAsync<AccountClaimMagicLinkOutput[]> {
|
|
259
|
+
return this.repository.auth.listAccountClaimMagicLinks(claimId);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
accountClaimOutputSchema,
|
|
4
|
+
accountClaimMagicLinkOutputSchema,
|
|
5
|
+
accountClaimSchema,
|
|
6
|
+
waitlistOutputSchema,
|
|
7
|
+
waitlistSchema,
|
|
8
|
+
} from "#modules/auth/auth.dto";
|
|
9
|
+
import type { AuthService } from "#modules/auth/auth.service";
|
|
10
|
+
import { adminProcedure, handleTRPCResult, procedure, publicProcedure, router } from "#trpc";
|
|
11
|
+
|
|
12
|
+
export function createAuthTRPC(authService: AuthService) {
|
|
13
|
+
return router({
|
|
14
|
+
getUserWaitlistCount: procedure.output(z.number()).query(async ({ ctx }) => {
|
|
15
|
+
return handleTRPCResult(await authService.getUserWaitlistCount(ctx));
|
|
16
|
+
}),
|
|
17
|
+
|
|
18
|
+
createInvitationCode: procedure
|
|
19
|
+
.input(z.object({ name: z.string().optional() }))
|
|
20
|
+
.output(waitlistSchema)
|
|
21
|
+
.mutation(async ({ input, ctx }) => {
|
|
22
|
+
return handleTRPCResult(await authService.createInvitationCode(input, ctx));
|
|
23
|
+
}),
|
|
24
|
+
|
|
25
|
+
createAccountClaimCode: adminProcedure
|
|
26
|
+
.input(
|
|
27
|
+
z.object({
|
|
28
|
+
userId: z.string(),
|
|
29
|
+
expiresInHours: z.number().optional(),
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
.output(accountClaimSchema)
|
|
33
|
+
.mutation(async ({ input }) => {
|
|
34
|
+
return handleTRPCResult(await authService.createAccountClaimCode(input));
|
|
35
|
+
}),
|
|
36
|
+
|
|
37
|
+
listAccountClaims: adminProcedure.output(z.array(accountClaimOutputSchema)).query(async () => {
|
|
38
|
+
return handleTRPCResult(await authService.listAccountClaims());
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
generateAccountClaimMagicLink: adminProcedure
|
|
42
|
+
.input(
|
|
43
|
+
z.object({
|
|
44
|
+
claimId: z.string(),
|
|
45
|
+
email: z.string().email().optional(),
|
|
46
|
+
})
|
|
47
|
+
)
|
|
48
|
+
.output(accountClaimMagicLinkOutputSchema)
|
|
49
|
+
.mutation(async ({ input }) => {
|
|
50
|
+
return handleTRPCResult(await authService.generateAccountClaimMagicLink(input));
|
|
51
|
+
}),
|
|
52
|
+
|
|
53
|
+
listAccountClaimMagicLinks: adminProcedure
|
|
54
|
+
.input(
|
|
55
|
+
z.object({
|
|
56
|
+
claimId: z.string(),
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
.output(z.array(accountClaimMagicLinkOutputSchema))
|
|
60
|
+
.query(async ({ input }) => {
|
|
61
|
+
return handleTRPCResult(await authService.listAccountClaimMagicLinks(input));
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
getMyAccountClaimStatus: procedure.output(accountClaimSchema.nullable()).query(async ({ ctx }) => {
|
|
65
|
+
return handleTRPCResult(await authService.getMyAccountClaimStatus(ctx));
|
|
66
|
+
}),
|
|
67
|
+
|
|
68
|
+
setMyAccountClaimEmail: procedure
|
|
69
|
+
.input(
|
|
70
|
+
z.object({
|
|
71
|
+
email: z.string().email(),
|
|
72
|
+
})
|
|
73
|
+
)
|
|
74
|
+
.output(z.object({ status: z.boolean() }))
|
|
75
|
+
.mutation(async ({ input, ctx }) => {
|
|
76
|
+
return handleTRPCResult(await authService.setMyAccountClaimEmail(input, ctx));
|
|
77
|
+
}),
|
|
78
|
+
|
|
79
|
+
acceptMyAccountClaim: procedure
|
|
80
|
+
.output(z.object({ status: z.boolean() }))
|
|
81
|
+
.mutation(async ({ ctx }) => {
|
|
82
|
+
return handleTRPCResult(await authService.acceptMyAccountClaim(ctx));
|
|
83
|
+
}),
|
|
84
|
+
|
|
85
|
+
listWaitlist: procedure.output(z.array(waitlistSchema)).query(async ({ ctx }) => {
|
|
86
|
+
return handleTRPCResult(await authService.listWaitlist(ctx));
|
|
87
|
+
}),
|
|
88
|
+
|
|
89
|
+
listAdminWaitlist: adminProcedure.output(z.array(waitlistOutputSchema)).query(async () => {
|
|
90
|
+
return handleTRPCResult(await authService.listAdminWaitlist());
|
|
91
|
+
}),
|
|
92
|
+
|
|
93
|
+
addToWaitlist: adminProcedure
|
|
94
|
+
.input(
|
|
95
|
+
z.object({
|
|
96
|
+
email: z.string(),
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
.output(waitlistOutputSchema)
|
|
100
|
+
.mutation(async ({ input }) => {
|
|
101
|
+
return handleTRPCResult(await authService.addToWaitlist(input));
|
|
102
|
+
}),
|
|
103
|
+
|
|
104
|
+
inviteToWaitlist: procedure
|
|
105
|
+
.input(
|
|
106
|
+
z.object({
|
|
107
|
+
email: z.string(),
|
|
108
|
+
name: z.string().optional(),
|
|
109
|
+
})
|
|
110
|
+
)
|
|
111
|
+
.output(waitlistSchema)
|
|
112
|
+
.mutation(async ({ input, ctx }) => {
|
|
113
|
+
return handleTRPCResult(await authService.inviteToWaitlist(input, ctx));
|
|
114
|
+
}),
|
|
115
|
+
|
|
116
|
+
inviteFromWaitlist: adminProcedure
|
|
117
|
+
.input(
|
|
118
|
+
z.object({
|
|
119
|
+
id: z.string(),
|
|
120
|
+
})
|
|
121
|
+
)
|
|
122
|
+
.output(waitlistOutputSchema)
|
|
123
|
+
.mutation(async ({ input }) => {
|
|
124
|
+
return handleTRPCResult(await authService.inviteFromWaitlist(input));
|
|
125
|
+
}),
|
|
126
|
+
|
|
127
|
+
removeFromWaitlist: adminProcedure
|
|
128
|
+
.input(
|
|
129
|
+
z.object({
|
|
130
|
+
id: z.string(),
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
.output(waitlistOutputSchema)
|
|
134
|
+
.mutation(async ({ input }) => {
|
|
135
|
+
return handleTRPCResult(await authService.removeFromWaitlist(input));
|
|
136
|
+
}),
|
|
137
|
+
|
|
138
|
+
joinWaitlist: publicProcedure
|
|
139
|
+
.input(
|
|
140
|
+
z.object({
|
|
141
|
+
email: z.string(),
|
|
142
|
+
})
|
|
143
|
+
)
|
|
144
|
+
.output(waitlistOutputSchema)
|
|
145
|
+
.mutation(async ({ input }) => {
|
|
146
|
+
return handleTRPCResult(await authService.joinWaitlist(input));
|
|
147
|
+
}),
|
|
148
|
+
|
|
149
|
+
getOnboarding: procedure.output(z.number()).query(async ({ ctx }) => {
|
|
150
|
+
return handleTRPCResult(await authService.getOnboarding(ctx));
|
|
151
|
+
}),
|
|
152
|
+
|
|
153
|
+
setOnboarding: procedure
|
|
154
|
+
.input(z.number())
|
|
155
|
+
.output(z.number())
|
|
156
|
+
.mutation(async ({ ctx, input }) => {
|
|
157
|
+
return handleTRPCResult(await authService.setOnboarding(input, ctx));
|
|
158
|
+
}),
|
|
159
|
+
|
|
160
|
+
getPreferences: procedure.output(z.record(z.string(), z.unknown())).query(async ({ ctx }) => {
|
|
161
|
+
return handleTRPCResult(await authService.getPreferences(ctx));
|
|
162
|
+
}),
|
|
163
|
+
|
|
164
|
+
setPreferences: procedure
|
|
165
|
+
.input(z.record(z.string(), z.unknown()))
|
|
166
|
+
.output(z.record(z.string(), z.unknown()))
|
|
167
|
+
.mutation(async ({ ctx, input }) => {
|
|
168
|
+
return handleTRPCResult(await authService.setPreferences(input, ctx));
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
getMetadata: procedure.output(z.record(z.string(), z.unknown())).query(async ({ ctx }) => {
|
|
172
|
+
return handleTRPCResult(await authService.getMetadata(ctx));
|
|
173
|
+
}),
|
|
174
|
+
|
|
175
|
+
setMetadata: procedure
|
|
176
|
+
.input(z.record(z.string(), z.unknown()))
|
|
177
|
+
.output(z.record(z.string(), z.unknown()))
|
|
178
|
+
.mutation(async ({ ctx, input }) => {
|
|
179
|
+
return handleTRPCResult(await authService.setMetadata(input, ctx));
|
|
180
|
+
}),
|
|
181
|
+
|
|
182
|
+
getFlags: procedure.output(z.array(z.string())).query(async ({ ctx }) => {
|
|
183
|
+
return handleTRPCResult(await authService.getFlags(ctx));
|
|
184
|
+
}),
|
|
185
|
+
|
|
186
|
+
setFlags: procedure
|
|
187
|
+
.input(z.array(z.string()))
|
|
188
|
+
.output(z.array(z.string()))
|
|
189
|
+
.mutation(async ({ ctx, input }) => {
|
|
190
|
+
return handleTRPCResult(await authService.setFlags(input, ctx));
|
|
191
|
+
}),
|
|
192
|
+
|
|
193
|
+
validateWaitlistCode: publicProcedure
|
|
194
|
+
.input(
|
|
195
|
+
z.object({
|
|
196
|
+
code: z.string(),
|
|
197
|
+
})
|
|
198
|
+
)
|
|
199
|
+
.output(
|
|
200
|
+
z.object({
|
|
201
|
+
status: z.string(),
|
|
202
|
+
})
|
|
203
|
+
)
|
|
204
|
+
.query(async ({ input }) => {
|
|
205
|
+
return handleTRPCResult(await authService.validateWaitlistCode(input.code));
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { desc, eq } from "drizzle-orm";
|
|
2
|
+
import type { LibSQLDatabase } from "drizzle-orm/libsql";
|
|
3
|
+
import { v4 as uuidv4 } from "uuid";
|
|
4
|
+
import * as auth from "#modules/auth/auth.db";
|
|
5
|
+
|
|
6
|
+
const schema = { ...auth };
|
|
7
|
+
type Schema = typeof schema;
|
|
8
|
+
export type Orm = LibSQLDatabase<Schema>;
|
|
9
|
+
|
|
10
|
+
export async function getActiveOrganizationAndTeam<O extends Orm, S extends Schema>(
|
|
11
|
+
orm: O,
|
|
12
|
+
schema: S,
|
|
13
|
+
userId: string
|
|
14
|
+
): Promise<{
|
|
15
|
+
organizationId: string | undefined;
|
|
16
|
+
teamId: string | undefined;
|
|
17
|
+
organizationRole: string | undefined;
|
|
18
|
+
teamRole: string | undefined;
|
|
19
|
+
}> {
|
|
20
|
+
let organizationId: string | undefined;
|
|
21
|
+
let teamId: string | undefined;
|
|
22
|
+
let organizationRole: string | undefined;
|
|
23
|
+
let teamRole: string | undefined;
|
|
24
|
+
const [lastSession] = await orm
|
|
25
|
+
.select({
|
|
26
|
+
activeOrganizationId: schema.sessions.activeOrganizationId,
|
|
27
|
+
activeTeamId: schema.sessions.activeTeamId,
|
|
28
|
+
activeOrganizationRole: schema.sessions.activeOrganizationRole,
|
|
29
|
+
activeTeamRole: schema.sessions.activeTeamRole,
|
|
30
|
+
})
|
|
31
|
+
.from(schema.sessions)
|
|
32
|
+
.where(eq(schema.sessions.userId, userId))
|
|
33
|
+
.orderBy(desc(schema.sessions.createdAt))
|
|
34
|
+
.limit(1);
|
|
35
|
+
if (lastSession) {
|
|
36
|
+
organizationId = lastSession.activeOrganizationId ?? undefined;
|
|
37
|
+
teamId = lastSession.activeTeamId ?? undefined;
|
|
38
|
+
organizationRole = lastSession.activeOrganizationRole ?? undefined;
|
|
39
|
+
teamRole = lastSession.activeTeamRole ?? undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!organizationId || !organizationRole) {
|
|
43
|
+
const [member] = await orm
|
|
44
|
+
.select({ organizationId: schema.members.organizationId, role: schema.members.role })
|
|
45
|
+
.from(schema.members)
|
|
46
|
+
.orderBy(desc(schema.members.createdAt))
|
|
47
|
+
.where(eq(schema.members.userId, userId))
|
|
48
|
+
.limit(1);
|
|
49
|
+
organizationId = member?.organizationId;
|
|
50
|
+
organizationRole = member?.role;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!teamId || !teamRole) {
|
|
54
|
+
const [teamMember] = await orm
|
|
55
|
+
.select({ teamId: schema.teamMembers.teamId, role: schema.teamMembers.role })
|
|
56
|
+
.from(schema.teamMembers)
|
|
57
|
+
.orderBy(desc(schema.teamMembers.createdAt))
|
|
58
|
+
.where(eq(schema.teamMembers.userId, userId))
|
|
59
|
+
.limit(1);
|
|
60
|
+
teamId = teamMember?.teamId;
|
|
61
|
+
teamRole = teamMember?.role;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { organizationId, teamId, organizationRole, teamRole };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function createOrganizationAndTeam<O extends Orm, S extends Schema>(
|
|
68
|
+
orm: O,
|
|
69
|
+
schema: S,
|
|
70
|
+
user: { id: string; email: string }
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
const organizationId = uuidv4();
|
|
73
|
+
await orm.transaction(async (tx) => {
|
|
74
|
+
const [organization] = await tx
|
|
75
|
+
.insert(schema.organizations)
|
|
76
|
+
.values({
|
|
77
|
+
id: organizationId,
|
|
78
|
+
name: organizationId,
|
|
79
|
+
slug: organizationId,
|
|
80
|
+
})
|
|
81
|
+
.returning();
|
|
82
|
+
|
|
83
|
+
if (!organization) throw new Error("createOrganizationAndTeam: Failed to create organization");
|
|
84
|
+
|
|
85
|
+
const [member] = await tx
|
|
86
|
+
.insert(schema.members)
|
|
87
|
+
.values({
|
|
88
|
+
userId: user.id,
|
|
89
|
+
organizationId: organization.id,
|
|
90
|
+
role: "owner",
|
|
91
|
+
})
|
|
92
|
+
.returning();
|
|
93
|
+
|
|
94
|
+
if (!member) throw new Error("createOrganizationAndTeam: Failed to create member");
|
|
95
|
+
|
|
96
|
+
const [team] = await tx
|
|
97
|
+
.insert(schema.teams)
|
|
98
|
+
.values({
|
|
99
|
+
name: organization.id,
|
|
100
|
+
organizationId: organization.id,
|
|
101
|
+
})
|
|
102
|
+
.returning();
|
|
103
|
+
|
|
104
|
+
if (!team) throw new Error("createOrganizationAndTeam: Failed to create team");
|
|
105
|
+
|
|
106
|
+
const [teamMember] = await tx
|
|
107
|
+
.insert(schema.teamMembers)
|
|
108
|
+
.values({
|
|
109
|
+
userId: user.id,
|
|
110
|
+
teamId: team.id,
|
|
111
|
+
role: "owner",
|
|
112
|
+
})
|
|
113
|
+
.returning();
|
|
114
|
+
|
|
115
|
+
if (!teamMember) throw new Error("createOrganizationAndTeam: Failed to create team member");
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { TRPC_ERROR_CODE_KEY } from "@trpc/server";
|
|
2
|
+
import { err } from "neverthrow";
|
|
3
|
+
import type { ServerResult, ServerResultAsync } from "#modules/base/base.dto";
|
|
4
|
+
import type { ServerErrorLayer } from "#modules/base/base.types";
|
|
5
|
+
import { reportError, ServerError } from "#utils/errors";
|
|
6
|
+
import { logger } from "#utils/logger";
|
|
7
|
+
|
|
8
|
+
export abstract class Base {
|
|
9
|
+
public layer: ServerErrorLayer;
|
|
10
|
+
public logger: ReturnType<typeof logger.child>;
|
|
11
|
+
|
|
12
|
+
constructor(layer: ServerErrorLayer) {
|
|
13
|
+
this.layer = layer;
|
|
14
|
+
this.logger = logger.child({ layer: this.layer, layerName: this.constructor.name });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
error(
|
|
18
|
+
code: TRPC_ERROR_CODE_KEY,
|
|
19
|
+
message?: string,
|
|
20
|
+
{
|
|
21
|
+
cause,
|
|
22
|
+
clientMessage,
|
|
23
|
+
log = process.env.NODE_ENV === "development",
|
|
24
|
+
}: { cause?: unknown; clientMessage?: string; log?: boolean } = {}
|
|
25
|
+
) {
|
|
26
|
+
const serverError = new ServerError({
|
|
27
|
+
code,
|
|
28
|
+
layer: this.layer,
|
|
29
|
+
layerName: this.constructor.name,
|
|
30
|
+
message,
|
|
31
|
+
clientMessage,
|
|
32
|
+
cause,
|
|
33
|
+
captureBoundary: true,
|
|
34
|
+
});
|
|
35
|
+
if (serverError.is5xxError()) reportError(serverError);
|
|
36
|
+
if (log) logger.error(serverError);
|
|
37
|
+
return err(serverError);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
handleUnknownError(error: unknown) {
|
|
41
|
+
return ServerError.fromUnknown("INTERNAL_SERVER_ERROR", error, {
|
|
42
|
+
layer: this.layer,
|
|
43
|
+
layerName: this.constructor.name,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throwable<T>(fn: () => ServerResult<T>): ServerResult<T> {
|
|
48
|
+
try {
|
|
49
|
+
return fn();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return err(this.handleUnknownError(error));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async throwableAsync<T>(fn: () => ServerResultAsync<T>): ServerResultAsync<T> {
|
|
56
|
+
try {
|
|
57
|
+
return fn();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return err(this.handleUnknownError(error));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|