@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,672 @@
|
|
|
1
|
+
import { and, count, desc, eq, gte, ne } from "drizzle-orm";
|
|
2
|
+
import type { LibSQLDatabase } from "drizzle-orm/libsql";
|
|
3
|
+
import { ok } from "neverthrow";
|
|
4
|
+
import { v4 as uuidv4 } from "uuid";
|
|
5
|
+
import * as auth from "#modules/auth/auth.db";
|
|
6
|
+
import type {
|
|
7
|
+
AccountClaim,
|
|
8
|
+
AccountClaimMagicLink,
|
|
9
|
+
AccountClaimMagicLinkOutput,
|
|
10
|
+
AccountClaimOutput,
|
|
11
|
+
Waitlist,
|
|
12
|
+
WaitlistOutput,
|
|
13
|
+
} from "#modules/auth/auth.dto";
|
|
14
|
+
import type { ServerResultAsync } from "#modules/base/base.dto";
|
|
15
|
+
import { BaseRepository } from "#modules/base/base.repository";
|
|
16
|
+
|
|
17
|
+
const schema = { ...auth };
|
|
18
|
+
type Schema = typeof schema;
|
|
19
|
+
type Orm = LibSQLDatabase<Schema>;
|
|
20
|
+
type UserRow = typeof auth.users.$inferSelect;
|
|
21
|
+
|
|
22
|
+
export class AuthRepository extends BaseRepository<Orm, Schema, Record<string, never>> {
|
|
23
|
+
async getUserWaitlistCount(userId: string, tx?: Orm): ServerResultAsync<number> {
|
|
24
|
+
return this.throwableAsync(async () => {
|
|
25
|
+
const db = tx ?? this.orm;
|
|
26
|
+
const [waitlist] = await db
|
|
27
|
+
.select({ count: count() })
|
|
28
|
+
.from(this.schema.waitlist)
|
|
29
|
+
.where(eq(this.schema.waitlist.userId, userId));
|
|
30
|
+
|
|
31
|
+
return ok(waitlist.count ?? 0);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getOnboarding(userId: string, tx?: Orm): ServerResultAsync<number> {
|
|
36
|
+
return this.throwableAsync(async () => {
|
|
37
|
+
const db = tx ?? this.orm;
|
|
38
|
+
const [user] = await db
|
|
39
|
+
.select({ onboarding: this.schema.users.onboarding })
|
|
40
|
+
.from(this.schema.users)
|
|
41
|
+
.where(eq(this.schema.users.id, userId))
|
|
42
|
+
.limit(1);
|
|
43
|
+
if (!user) return this.error("FORBIDDEN");
|
|
44
|
+
|
|
45
|
+
return ok(user.onboarding ?? 0);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async setOnboarding(userId: string, onboarding: number, tx?: Orm): ServerResultAsync<number> {
|
|
50
|
+
return this.throwableAsync(async () => {
|
|
51
|
+
const db = tx ?? this.orm;
|
|
52
|
+
await db
|
|
53
|
+
.update(this.schema.users)
|
|
54
|
+
.set({ onboarding })
|
|
55
|
+
.where(eq(this.schema.users.id, userId));
|
|
56
|
+
return ok(onboarding);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getPreferences(userId: string, tx?: Orm): ServerResultAsync<Record<string, unknown>> {
|
|
61
|
+
return this.throwableAsync(async () => {
|
|
62
|
+
const db = tx ?? this.orm;
|
|
63
|
+
const [user] = await db
|
|
64
|
+
.select({ preferences: this.schema.users.preferences })
|
|
65
|
+
.from(this.schema.users)
|
|
66
|
+
.where(eq(this.schema.users.id, userId))
|
|
67
|
+
.limit(1);
|
|
68
|
+
if (!user) return this.error("FORBIDDEN");
|
|
69
|
+
const json = user.preferences
|
|
70
|
+
? (JSON.parse(user.preferences) as Record<string, unknown>)
|
|
71
|
+
: {};
|
|
72
|
+
return ok(json);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async setPreferences(
|
|
77
|
+
userId: string,
|
|
78
|
+
preferences: Record<string, unknown>,
|
|
79
|
+
tx?: Orm
|
|
80
|
+
): ServerResultAsync<Record<string, unknown>> {
|
|
81
|
+
return this.throwableAsync(async () => {
|
|
82
|
+
const db = tx ?? this.orm;
|
|
83
|
+
await db
|
|
84
|
+
.update(this.schema.users)
|
|
85
|
+
.set({ preferences: JSON.stringify(preferences) })
|
|
86
|
+
.where(eq(this.schema.users.id, userId));
|
|
87
|
+
return ok(preferences);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async getMetadata(userId: string, tx?: Orm): ServerResultAsync<Record<string, unknown>> {
|
|
92
|
+
return this.throwableAsync(async () => {
|
|
93
|
+
const db = tx ?? this.orm;
|
|
94
|
+
const [user] = await db
|
|
95
|
+
.select({ metadata: this.schema.users.metadata })
|
|
96
|
+
.from(this.schema.users)
|
|
97
|
+
.where(eq(this.schema.users.id, userId))
|
|
98
|
+
.limit(1);
|
|
99
|
+
if (!user) return this.error("FORBIDDEN");
|
|
100
|
+
|
|
101
|
+
return ok(user.metadata);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async setMetadata(
|
|
106
|
+
userId: string,
|
|
107
|
+
metadata: Record<string, unknown>,
|
|
108
|
+
tx?: Orm
|
|
109
|
+
): ServerResultAsync<Record<string, unknown>> {
|
|
110
|
+
return this.throwableAsync(async () => {
|
|
111
|
+
const db = tx ?? this.orm;
|
|
112
|
+
const [user] = await db
|
|
113
|
+
.select({ metadata: this.schema.users.metadata })
|
|
114
|
+
.from(this.schema.users)
|
|
115
|
+
.where(eq(this.schema.users.id, userId))
|
|
116
|
+
.limit(1);
|
|
117
|
+
if (!user) return this.error("FORBIDDEN");
|
|
118
|
+
await db
|
|
119
|
+
.update(this.schema.users)
|
|
120
|
+
.set({
|
|
121
|
+
metadata: {
|
|
122
|
+
...user.metadata,
|
|
123
|
+
...metadata,
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
.where(eq(this.schema.users.id, userId));
|
|
127
|
+
return ok(metadata);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async getFlags(userId: string, tx?: Orm): ServerResultAsync<string[]> {
|
|
132
|
+
return this.throwableAsync(async () => {
|
|
133
|
+
const db = tx ?? this.orm;
|
|
134
|
+
const [user] = await db
|
|
135
|
+
.select({ flags: this.schema.users.flags })
|
|
136
|
+
.from(this.schema.users)
|
|
137
|
+
.where(eq(this.schema.users.id, userId))
|
|
138
|
+
.limit(1);
|
|
139
|
+
if (!user) return this.error("FORBIDDEN");
|
|
140
|
+
const json = user.flags ? (JSON.parse(user.flags) as string[]) : [];
|
|
141
|
+
|
|
142
|
+
return ok(json);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async setFlags(userId: string, flags: string[], tx?: Orm): ServerResultAsync<string[]> {
|
|
147
|
+
return this.throwableAsync(async () => {
|
|
148
|
+
const db = tx ?? this.orm;
|
|
149
|
+
await db
|
|
150
|
+
.update(this.schema.users)
|
|
151
|
+
.set({ flags: JSON.stringify(flags) })
|
|
152
|
+
.where(eq(this.schema.users.id, userId));
|
|
153
|
+
return ok(flags);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async listAdminWaitlist(tx?: Orm): ServerResultAsync<WaitlistOutput[]> {
|
|
158
|
+
return this.throwableAsync(async () => {
|
|
159
|
+
const db = tx ?? this.orm;
|
|
160
|
+
const waitlist = await db
|
|
161
|
+
.select({
|
|
162
|
+
id: this.schema.waitlist.id,
|
|
163
|
+
name: this.schema.waitlist.name,
|
|
164
|
+
email: this.schema.waitlist.email,
|
|
165
|
+
createdAt: this.schema.waitlist.createdAt,
|
|
166
|
+
updatedAt: this.schema.waitlist.updatedAt,
|
|
167
|
+
status: this.schema.waitlist.status,
|
|
168
|
+
})
|
|
169
|
+
.from(this.schema.waitlist)
|
|
170
|
+
.where(eq(this.schema.waitlist.type, "WAITLIST"))
|
|
171
|
+
.orderBy(desc(this.schema.waitlist.createdAt));
|
|
172
|
+
return ok(waitlist);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async listWaitlist(userId: string, tx?: Orm): ServerResultAsync<Waitlist[]> {
|
|
177
|
+
return this.throwableAsync(async () => {
|
|
178
|
+
const db = tx ?? this.orm;
|
|
179
|
+
const waitlist = await db
|
|
180
|
+
.select()
|
|
181
|
+
.from(this.schema.waitlist)
|
|
182
|
+
.where(
|
|
183
|
+
and(eq(this.schema.waitlist.userId, userId), eq(this.schema.waitlist.type, "WAITLIST"))
|
|
184
|
+
);
|
|
185
|
+
return ok(waitlist);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async addToWaitlist(email: string, tx?: Orm): ServerResultAsync<WaitlistOutput> {
|
|
190
|
+
return this.throwableAsync(async () => {
|
|
191
|
+
const db = tx ?? this.orm;
|
|
192
|
+
const [waitlist] = await db.insert(this.schema.waitlist).values({ email }).returning();
|
|
193
|
+
return ok(waitlist);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async inviteFromWaitlist(id: string, tx?: Orm): ServerResultAsync<Waitlist> {
|
|
198
|
+
return this.throwableAsync(async () => {
|
|
199
|
+
const db = tx ?? this.orm;
|
|
200
|
+
const [waitlist] = await db
|
|
201
|
+
.update(this.schema.waitlist)
|
|
202
|
+
.set({
|
|
203
|
+
status: "INVITED",
|
|
204
|
+
code: uuidv4(),
|
|
205
|
+
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 14),
|
|
206
|
+
})
|
|
207
|
+
.where(eq(this.schema.waitlist.id, id))
|
|
208
|
+
.returning();
|
|
209
|
+
return ok(waitlist);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async inviteToWaitlist(
|
|
214
|
+
{ email, userId, name }: { email: string; userId: string; name?: string },
|
|
215
|
+
tx?: Orm
|
|
216
|
+
): ServerResultAsync<Waitlist> {
|
|
217
|
+
return this.throwableAsync(async () => {
|
|
218
|
+
const db = tx ?? this.orm;
|
|
219
|
+
const [waitlist] = await db
|
|
220
|
+
.insert(this.schema.waitlist)
|
|
221
|
+
.values({
|
|
222
|
+
email,
|
|
223
|
+
name,
|
|
224
|
+
status: "INVITED",
|
|
225
|
+
code: uuidv4(),
|
|
226
|
+
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
|
|
227
|
+
userId: userId,
|
|
228
|
+
})
|
|
229
|
+
.returning();
|
|
230
|
+
return ok(waitlist);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async createInvitationCode(
|
|
235
|
+
{ userId, name }: { userId: string; name?: string },
|
|
236
|
+
tx?: Orm
|
|
237
|
+
): ServerResultAsync<Waitlist> {
|
|
238
|
+
return this.throwableAsync(async () => {
|
|
239
|
+
const db = tx ?? this.orm;
|
|
240
|
+
const [waitlist] = await db
|
|
241
|
+
.insert(this.schema.waitlist)
|
|
242
|
+
.values({
|
|
243
|
+
name,
|
|
244
|
+
status: "INVITED",
|
|
245
|
+
code: uuidv4(),
|
|
246
|
+
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
|
|
247
|
+
userId: userId,
|
|
248
|
+
})
|
|
249
|
+
.returning();
|
|
250
|
+
return ok(waitlist);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async joinWaitlist(email: string, tx?: Orm): ServerResultAsync<WaitlistOutput> {
|
|
255
|
+
return this.throwableAsync(async () => {
|
|
256
|
+
const db = tx ?? this.orm;
|
|
257
|
+
const [waitlist] = await db.insert(this.schema.waitlist).values({ email }).returning();
|
|
258
|
+
return ok(waitlist);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async removeFromWaitlist(id: string, tx?: Orm): ServerResultAsync<WaitlistOutput> {
|
|
263
|
+
return this.throwableAsync(async () => {
|
|
264
|
+
const db = tx ?? this.orm;
|
|
265
|
+
const [waitlist] = await db
|
|
266
|
+
.update(this.schema.waitlist)
|
|
267
|
+
.set({ status: "REMOVED" })
|
|
268
|
+
.where(eq(this.schema.waitlist.id, id))
|
|
269
|
+
.returning();
|
|
270
|
+
return ok(waitlist);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async validateWaitlistCode(code: string, tx?: Orm): ServerResultAsync<{ status: string }> {
|
|
275
|
+
return this.throwableAsync(async () => {
|
|
276
|
+
const db = tx ?? this.orm;
|
|
277
|
+
const [waitlist] = await db
|
|
278
|
+
.select()
|
|
279
|
+
.from(this.schema.waitlist)
|
|
280
|
+
.where(and(eq(this.schema.waitlist.code, code), eq(this.schema.waitlist.type, "WAITLIST")))
|
|
281
|
+
.limit(1);
|
|
282
|
+
if (!waitlist) return ok({ status: "NOT_FOUND" });
|
|
283
|
+
if (waitlist.expiresAt && waitlist.expiresAt < new Date()) return ok({ status: "EXPIRED" });
|
|
284
|
+
if (waitlist.status !== "INVITED") return ok({ status: "INVALID" });
|
|
285
|
+
return ok({ status: "VALID" });
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async createAccountClaimCode(
|
|
290
|
+
{ userId, expiresInHours = 24 * 14 }: { userId: string; expiresInHours?: number },
|
|
291
|
+
tx?: Orm
|
|
292
|
+
): ServerResultAsync<AccountClaim> {
|
|
293
|
+
return this.throwableAsync(async () => {
|
|
294
|
+
const db = tx ?? this.orm;
|
|
295
|
+
const [claim] = await db
|
|
296
|
+
.insert(this.schema.waitlist)
|
|
297
|
+
.values({
|
|
298
|
+
type: "ACCOUNT_CLAIM",
|
|
299
|
+
claimUserId: userId,
|
|
300
|
+
code: uuidv4(),
|
|
301
|
+
status: "INVITED",
|
|
302
|
+
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * expiresInHours),
|
|
303
|
+
})
|
|
304
|
+
.returning();
|
|
305
|
+
return ok(claim);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async createClaimableProvisionedUser({
|
|
310
|
+
name,
|
|
311
|
+
email,
|
|
312
|
+
metadata = {},
|
|
313
|
+
onboarding = 0,
|
|
314
|
+
role = "user",
|
|
315
|
+
expiresInHours = 24 * 14,
|
|
316
|
+
}: {
|
|
317
|
+
name: string;
|
|
318
|
+
email: string;
|
|
319
|
+
metadata?: Record<string, unknown>;
|
|
320
|
+
onboarding?: number;
|
|
321
|
+
role?: "user" | "admin" | "agent";
|
|
322
|
+
expiresInHours?: number;
|
|
323
|
+
}): ServerResultAsync<{ user: UserRow; claim: AccountClaim }> {
|
|
324
|
+
return this.throwableAsync(async () => {
|
|
325
|
+
const normalizedEmail = email.toLowerCase();
|
|
326
|
+
const [existingUser] = await this.orm
|
|
327
|
+
.select({ id: this.schema.users.id })
|
|
328
|
+
.from(this.schema.users)
|
|
329
|
+
.where(eq(this.schema.users.email, normalizedEmail))
|
|
330
|
+
.limit(1);
|
|
331
|
+
if (existingUser) {
|
|
332
|
+
return this.error("CONFLICT", "Email already in use");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const created = await this.orm.transaction(async (tx) => {
|
|
336
|
+
const [user] = await tx
|
|
337
|
+
.insert(this.schema.users)
|
|
338
|
+
.values({
|
|
339
|
+
name,
|
|
340
|
+
email: normalizedEmail,
|
|
341
|
+
emailVerified: false,
|
|
342
|
+
role,
|
|
343
|
+
onboarding,
|
|
344
|
+
metadata,
|
|
345
|
+
})
|
|
346
|
+
.returning();
|
|
347
|
+
if (!user) throw new Error("Failed to create user");
|
|
348
|
+
|
|
349
|
+
const organizationId = uuidv4();
|
|
350
|
+
const [organization] = await tx
|
|
351
|
+
.insert(this.schema.organizations)
|
|
352
|
+
.values({
|
|
353
|
+
id: organizationId,
|
|
354
|
+
name: organizationId,
|
|
355
|
+
slug: organizationId,
|
|
356
|
+
})
|
|
357
|
+
.returning();
|
|
358
|
+
if (!organization) throw new Error("Failed to create organization");
|
|
359
|
+
|
|
360
|
+
const [member] = await tx
|
|
361
|
+
.insert(this.schema.members)
|
|
362
|
+
.values({
|
|
363
|
+
userId: user.id,
|
|
364
|
+
organizationId: organization.id,
|
|
365
|
+
role: "owner",
|
|
366
|
+
})
|
|
367
|
+
.returning();
|
|
368
|
+
if (!member) throw new Error("Failed to create organization membership");
|
|
369
|
+
|
|
370
|
+
const [team] = await tx
|
|
371
|
+
.insert(this.schema.teams)
|
|
372
|
+
.values({
|
|
373
|
+
name: organization.id,
|
|
374
|
+
organizationId: organization.id,
|
|
375
|
+
})
|
|
376
|
+
.returning();
|
|
377
|
+
if (!team) throw new Error("Failed to create team");
|
|
378
|
+
|
|
379
|
+
const [teamMember] = await tx
|
|
380
|
+
.insert(this.schema.teamMembers)
|
|
381
|
+
.values({
|
|
382
|
+
userId: user.id,
|
|
383
|
+
teamId: team.id,
|
|
384
|
+
role: "owner",
|
|
385
|
+
})
|
|
386
|
+
.returning();
|
|
387
|
+
if (!teamMember) throw new Error("Failed to create team membership");
|
|
388
|
+
|
|
389
|
+
const [claim] = await tx
|
|
390
|
+
.insert(this.schema.waitlist)
|
|
391
|
+
.values({
|
|
392
|
+
type: "ACCOUNT_CLAIM",
|
|
393
|
+
claimUserId: user.id,
|
|
394
|
+
code: uuidv4(),
|
|
395
|
+
status: "INVITED",
|
|
396
|
+
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * expiresInHours),
|
|
397
|
+
})
|
|
398
|
+
.returning();
|
|
399
|
+
if (!claim) throw new Error("Failed to create account claim");
|
|
400
|
+
|
|
401
|
+
return { user, claim };
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
return ok(created);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async listAccountClaims(tx?: Orm): ServerResultAsync<AccountClaimOutput[]> {
|
|
409
|
+
return this.throwableAsync(async () => {
|
|
410
|
+
const db = tx ?? this.orm;
|
|
411
|
+
const claims = await db
|
|
412
|
+
.select({
|
|
413
|
+
id: this.schema.waitlist.id,
|
|
414
|
+
claimUserId: this.schema.waitlist.claimUserId,
|
|
415
|
+
status: this.schema.waitlist.status,
|
|
416
|
+
expiresAt: this.schema.waitlist.expiresAt,
|
|
417
|
+
claimedAt: this.schema.waitlist.claimedAt,
|
|
418
|
+
claimedEmail: this.schema.waitlist.claimedEmail,
|
|
419
|
+
createdAt: this.schema.waitlist.createdAt,
|
|
420
|
+
updatedAt: this.schema.waitlist.updatedAt,
|
|
421
|
+
})
|
|
422
|
+
.from(this.schema.waitlist)
|
|
423
|
+
.where(eq(this.schema.waitlist.type, "ACCOUNT_CLAIM"))
|
|
424
|
+
.orderBy(desc(this.schema.waitlist.createdAt));
|
|
425
|
+
return ok(claims);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async validateAccountClaimCode(code: string, tx?: Orm): ServerResultAsync<{ status: string }> {
|
|
430
|
+
return this.throwableAsync(async () => {
|
|
431
|
+
const db = tx ?? this.orm;
|
|
432
|
+
const [claim] = await db
|
|
433
|
+
.select()
|
|
434
|
+
.from(this.schema.waitlist)
|
|
435
|
+
.where(
|
|
436
|
+
and(eq(this.schema.waitlist.code, code), eq(this.schema.waitlist.type, "ACCOUNT_CLAIM"))
|
|
437
|
+
)
|
|
438
|
+
.limit(1);
|
|
439
|
+
if (!claim) return ok({ status: "NOT_FOUND" });
|
|
440
|
+
if (claim.expiresAt && claim.expiresAt < new Date()) return ok({ status: "EXPIRED" });
|
|
441
|
+
if (claim.status !== "INVITED") return ok({ status: "INVALID" });
|
|
442
|
+
return ok({ status: "VALID" });
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async findAccountClaimByCode(code: string, tx?: Orm): ServerResultAsync<AccountClaim | null> {
|
|
447
|
+
return this.throwableAsync(async () => {
|
|
448
|
+
const db = tx ?? this.orm;
|
|
449
|
+
const [claim] = await db
|
|
450
|
+
.select()
|
|
451
|
+
.from(this.schema.waitlist)
|
|
452
|
+
.where(
|
|
453
|
+
and(
|
|
454
|
+
eq(this.schema.waitlist.code, code),
|
|
455
|
+
eq(this.schema.waitlist.type, "ACCOUNT_CLAIM"),
|
|
456
|
+
eq(this.schema.waitlist.status, "INVITED"),
|
|
457
|
+
gte(this.schema.waitlist.expiresAt, new Date())
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
.limit(1);
|
|
461
|
+
return ok(claim ?? null);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async findAccountClaimById(id: string, tx?: Orm): ServerResultAsync<AccountClaim | null> {
|
|
466
|
+
return this.throwableAsync(async () => {
|
|
467
|
+
const db = tx ?? this.orm;
|
|
468
|
+
const [claim] = await db
|
|
469
|
+
.select()
|
|
470
|
+
.from(this.schema.waitlist)
|
|
471
|
+
.where(and(eq(this.schema.waitlist.id, id), eq(this.schema.waitlist.type, "ACCOUNT_CLAIM")))
|
|
472
|
+
.limit(1);
|
|
473
|
+
return ok(claim ?? null);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async findPendingAccountClaimForUser(
|
|
478
|
+
userId: string,
|
|
479
|
+
tx?: Orm
|
|
480
|
+
): ServerResultAsync<AccountClaim | null> {
|
|
481
|
+
return this.throwableAsync(async () => {
|
|
482
|
+
const db = tx ?? this.orm;
|
|
483
|
+
const [claim] = await db
|
|
484
|
+
.select()
|
|
485
|
+
.from(this.schema.waitlist)
|
|
486
|
+
.where(
|
|
487
|
+
and(
|
|
488
|
+
eq(this.schema.waitlist.type, "ACCOUNT_CLAIM"),
|
|
489
|
+
eq(this.schema.waitlist.claimUserId, userId),
|
|
490
|
+
eq(this.schema.waitlist.status, "INVITED"),
|
|
491
|
+
gte(this.schema.waitlist.expiresAt, new Date())
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
.orderBy(desc(this.schema.waitlist.createdAt))
|
|
495
|
+
.limit(1);
|
|
496
|
+
return ok(claim ?? null);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async setAccountClaimEmail(
|
|
501
|
+
{ userId, email }: { userId: string; email: string },
|
|
502
|
+
tx?: Orm
|
|
503
|
+
): ServerResultAsync<{ status: boolean }> {
|
|
504
|
+
return this.throwableAsync(async () => {
|
|
505
|
+
const db = tx ?? this.orm;
|
|
506
|
+
const normalizedEmail = email.toLowerCase();
|
|
507
|
+
const [existingUser] = await db
|
|
508
|
+
.select({ id: this.schema.users.id })
|
|
509
|
+
.from(this.schema.users)
|
|
510
|
+
.where(and(eq(this.schema.users.email, normalizedEmail), ne(this.schema.users.id, userId)))
|
|
511
|
+
.limit(1);
|
|
512
|
+
if (existingUser) {
|
|
513
|
+
return this.error("BAD_REQUEST", "Email is already in use");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const [claim] = await db
|
|
517
|
+
.select({ id: this.schema.waitlist.id })
|
|
518
|
+
.from(this.schema.waitlist)
|
|
519
|
+
.where(
|
|
520
|
+
and(
|
|
521
|
+
eq(this.schema.waitlist.type, "ACCOUNT_CLAIM"),
|
|
522
|
+
eq(this.schema.waitlist.claimUserId, userId),
|
|
523
|
+
eq(this.schema.waitlist.status, "INVITED"),
|
|
524
|
+
gte(this.schema.waitlist.expiresAt, new Date())
|
|
525
|
+
)
|
|
526
|
+
)
|
|
527
|
+
.orderBy(desc(this.schema.waitlist.createdAt))
|
|
528
|
+
.limit(1);
|
|
529
|
+
if (!claim) {
|
|
530
|
+
return this.error("BAD_REQUEST", "No pending claim found");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
await db
|
|
534
|
+
.update(this.schema.users)
|
|
535
|
+
.set({
|
|
536
|
+
email: normalizedEmail,
|
|
537
|
+
emailVerified: false,
|
|
538
|
+
updatedAt: new Date(),
|
|
539
|
+
})
|
|
540
|
+
.where(eq(this.schema.users.id, userId));
|
|
541
|
+
|
|
542
|
+
await db
|
|
543
|
+
.update(this.schema.waitlist)
|
|
544
|
+
.set({
|
|
545
|
+
claimedEmail: normalizedEmail,
|
|
546
|
+
updatedAt: new Date(),
|
|
547
|
+
})
|
|
548
|
+
.where(eq(this.schema.waitlist.id, claim.id));
|
|
549
|
+
|
|
550
|
+
return ok({ status: true });
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async acceptAccountClaim(userId: string, tx?: Orm): ServerResultAsync<{ status: boolean }> {
|
|
555
|
+
return this.throwableAsync(async () => {
|
|
556
|
+
const db = tx ?? this.orm;
|
|
557
|
+
const [claim] = await db
|
|
558
|
+
.select({ id: this.schema.waitlist.id })
|
|
559
|
+
.from(this.schema.waitlist)
|
|
560
|
+
.where(
|
|
561
|
+
and(
|
|
562
|
+
eq(this.schema.waitlist.type, "ACCOUNT_CLAIM"),
|
|
563
|
+
eq(this.schema.waitlist.claimUserId, userId),
|
|
564
|
+
eq(this.schema.waitlist.status, "INVITED"),
|
|
565
|
+
gte(this.schema.waitlist.expiresAt, new Date())
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
.orderBy(desc(this.schema.waitlist.createdAt))
|
|
569
|
+
.limit(1);
|
|
570
|
+
if (!claim) return ok({ status: true });
|
|
571
|
+
|
|
572
|
+
const [user] = await db
|
|
573
|
+
.select({ email: this.schema.users.email })
|
|
574
|
+
.from(this.schema.users)
|
|
575
|
+
.where(eq(this.schema.users.id, userId))
|
|
576
|
+
.limit(1);
|
|
577
|
+
|
|
578
|
+
await db
|
|
579
|
+
.update(this.schema.waitlist)
|
|
580
|
+
.set({
|
|
581
|
+
status: "ACCEPTED",
|
|
582
|
+
claimedAt: new Date(),
|
|
583
|
+
claimedEmail: user?.email ?? null,
|
|
584
|
+
updatedAt: new Date(),
|
|
585
|
+
})
|
|
586
|
+
.where(eq(this.schema.waitlist.id, claim.id));
|
|
587
|
+
return ok({ status: true });
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async createAccountClaimMagicLink(
|
|
592
|
+
{
|
|
593
|
+
claimId,
|
|
594
|
+
userId,
|
|
595
|
+
email,
|
|
596
|
+
token,
|
|
597
|
+
url,
|
|
598
|
+
expiresAt,
|
|
599
|
+
}: {
|
|
600
|
+
claimId: string;
|
|
601
|
+
userId: string;
|
|
602
|
+
email: string;
|
|
603
|
+
token: string;
|
|
604
|
+
url: string;
|
|
605
|
+
expiresAt?: Date;
|
|
606
|
+
},
|
|
607
|
+
tx?: Orm
|
|
608
|
+
): ServerResultAsync<AccountClaimMagicLink> {
|
|
609
|
+
return this.throwableAsync(async () => {
|
|
610
|
+
const db = tx ?? this.orm;
|
|
611
|
+
const [link] = await db
|
|
612
|
+
.insert(this.schema.accountClaimMagicLinks)
|
|
613
|
+
.values({
|
|
614
|
+
claimId,
|
|
615
|
+
userId,
|
|
616
|
+
email,
|
|
617
|
+
token,
|
|
618
|
+
url,
|
|
619
|
+
expiresAt: expiresAt ?? null,
|
|
620
|
+
})
|
|
621
|
+
.returning();
|
|
622
|
+
return ok(link);
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async listAccountClaimMagicLinks(
|
|
627
|
+
claimId: string,
|
|
628
|
+
tx?: Orm
|
|
629
|
+
): ServerResultAsync<AccountClaimMagicLinkOutput[]> {
|
|
630
|
+
return this.throwableAsync(async () => {
|
|
631
|
+
const db = tx ?? this.orm;
|
|
632
|
+
const links = await db
|
|
633
|
+
.select({
|
|
634
|
+
id: this.schema.accountClaimMagicLinks.id,
|
|
635
|
+
claimId: this.schema.accountClaimMagicLinks.claimId,
|
|
636
|
+
userId: this.schema.accountClaimMagicLinks.userId,
|
|
637
|
+
email: this.schema.accountClaimMagicLinks.email,
|
|
638
|
+
url: this.schema.accountClaimMagicLinks.url,
|
|
639
|
+
expiresAt: this.schema.accountClaimMagicLinks.expiresAt,
|
|
640
|
+
createdAt: this.schema.accountClaimMagicLinks.createdAt,
|
|
641
|
+
})
|
|
642
|
+
.from(this.schema.accountClaimMagicLinks)
|
|
643
|
+
.where(eq(this.schema.accountClaimMagicLinks.claimId, claimId))
|
|
644
|
+
.orderBy(desc(this.schema.accountClaimMagicLinks.createdAt));
|
|
645
|
+
return ok(links);
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async latestAccountClaimMagicLink(
|
|
650
|
+
claimId: string,
|
|
651
|
+
tx?: Orm
|
|
652
|
+
): ServerResultAsync<AccountClaimMagicLinkOutput | null> {
|
|
653
|
+
return this.throwableAsync(async () => {
|
|
654
|
+
const db = tx ?? this.orm;
|
|
655
|
+
const [link] = await db
|
|
656
|
+
.select({
|
|
657
|
+
id: this.schema.accountClaimMagicLinks.id,
|
|
658
|
+
claimId: this.schema.accountClaimMagicLinks.claimId,
|
|
659
|
+
userId: this.schema.accountClaimMagicLinks.userId,
|
|
660
|
+
email: this.schema.accountClaimMagicLinks.email,
|
|
661
|
+
url: this.schema.accountClaimMagicLinks.url,
|
|
662
|
+
expiresAt: this.schema.accountClaimMagicLinks.expiresAt,
|
|
663
|
+
createdAt: this.schema.accountClaimMagicLinks.createdAt,
|
|
664
|
+
})
|
|
665
|
+
.from(this.schema.accountClaimMagicLinks)
|
|
666
|
+
.where(eq(this.schema.accountClaimMagicLinks.claimId, claimId))
|
|
667
|
+
.orderBy(desc(this.schema.accountClaimMagicLinks.createdAt))
|
|
668
|
+
.limit(1);
|
|
669
|
+
return ok(link ?? null);
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|