@goscribe/server 1.6.0 → 1.7.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/dist/generated/prisma/client.d.ts +224 -0
- package/dist/generated/prisma/client.js +34 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +941 -0
- package/dist/generated/prisma/commonInputTypes.js +10 -0
- package/dist/generated/prisma/enums.d.ts +67 -0
- package/dist/generated/prisma/enums.js +66 -0
- package/dist/generated/prisma/internal/class.d.ts +539 -0
- package/dist/generated/prisma/internal/class.js +49 -0
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +3924 -0
- package/dist/generated/prisma/internal/prismaNamespace.js +557 -0
- package/dist/generated/prisma/models/ActivityLog.d.ts +1847 -0
- package/dist/generated/prisma/models/ActivityLog.js +1 -0
- package/dist/generated/prisma/models/Artifact.d.ts +2345 -0
- package/dist/generated/prisma/models/Artifact.js +1 -0
- package/dist/generated/prisma/models/ArtifactVersion.d.ts +1550 -0
- package/dist/generated/prisma/models/ArtifactVersion.js +1 -0
- package/dist/generated/prisma/models/Channel.d.ts +1257 -0
- package/dist/generated/prisma/models/Channel.js +1 -0
- package/dist/generated/prisma/models/Chat.d.ts +1339 -0
- package/dist/generated/prisma/models/Chat.js +1 -0
- package/dist/generated/prisma/models/CopilotConversation.d.ts +1450 -0
- package/dist/generated/prisma/models/CopilotConversation.js +1 -0
- package/dist/generated/prisma/models/CopilotMessage.d.ts +1179 -0
- package/dist/generated/prisma/models/CopilotMessage.js +1 -0
- package/dist/generated/prisma/models/FileAsset.d.ts +1832 -0
- package/dist/generated/prisma/models/FileAsset.js +1 -0
- package/dist/generated/prisma/models/Flashcard.d.ts +1460 -0
- package/dist/generated/prisma/models/Flashcard.js +1 -0
- package/dist/generated/prisma/models/FlashcardProgress.d.ts +1782 -0
- package/dist/generated/prisma/models/FlashcardProgress.js +1 -0
- package/dist/generated/prisma/models/Folder.d.ts +1685 -0
- package/dist/generated/prisma/models/Folder.js +1 -0
- package/dist/generated/prisma/models/IdempotencyRecord.d.ts +1319 -0
- package/dist/generated/prisma/models/IdempotencyRecord.js +1 -0
- package/dist/generated/prisma/models/Invoice.d.ts +1586 -0
- package/dist/generated/prisma/models/Invoice.js +1 -0
- package/dist/generated/prisma/models/KnowledgeBase.d.ts +1721 -0
- package/dist/generated/prisma/models/KnowledgeBase.js +1 -0
- package/dist/generated/prisma/models/KnowledgeBaseChunk.d.ts +1333 -0
- package/dist/generated/prisma/models/KnowledgeBaseChunk.js +1 -0
- package/dist/generated/prisma/models/KnowledgeBaseDocument.d.ts +1695 -0
- package/dist/generated/prisma/models/KnowledgeBaseDocument.js +1 -0
- package/dist/generated/prisma/models/Notification.d.ts +1992 -0
- package/dist/generated/prisma/models/Notification.js +1 -0
- package/dist/generated/prisma/models/PasswordResetToken.d.ts +1210 -0
- package/dist/generated/prisma/models/PasswordResetToken.js +1 -0
- package/dist/generated/prisma/models/Plan.d.ts +1431 -0
- package/dist/generated/prisma/models/Plan.js +1 -0
- package/dist/generated/prisma/models/PlanLimit.d.ts +1328 -0
- package/dist/generated/prisma/models/PlanLimit.js +1 -0
- package/dist/generated/prisma/models/PodcastSegment.d.ts +1564 -0
- package/dist/generated/prisma/models/PodcastSegment.js +1 -0
- package/dist/generated/prisma/models/ResourcePrice.d.ts +1008 -0
- package/dist/generated/prisma/models/ResourcePrice.js +1 -0
- package/dist/generated/prisma/models/Role.d.ts +1065 -0
- package/dist/generated/prisma/models/Role.js +1 -0
- package/dist/generated/prisma/models/Session.d.ts +1105 -0
- package/dist/generated/prisma/models/Session.js +1 -0
- package/dist/generated/prisma/models/StripeEvent.d.ts +1081 -0
- package/dist/generated/prisma/models/StripeEvent.js +1 -0
- package/dist/generated/prisma/models/StudyGuideComment.d.ts +1321 -0
- package/dist/generated/prisma/models/StudyGuideComment.js +1 -0
- package/dist/generated/prisma/models/StudyGuideHighlight.d.ts +1629 -0
- package/dist/generated/prisma/models/StudyGuideHighlight.js +1 -0
- package/dist/generated/prisma/models/Subscription.d.ts +1677 -0
- package/dist/generated/prisma/models/Subscription.js +1 -0
- package/dist/generated/prisma/models/User.d.ts +7559 -0
- package/dist/generated/prisma/models/User.js +1 -0
- package/dist/generated/prisma/models/UserCredit.d.ts +1249 -0
- package/dist/generated/prisma/models/UserCredit.js +1 -0
- package/dist/generated/prisma/models/VerificationToken.d.ts +946 -0
- package/dist/generated/prisma/models/VerificationToken.js +1 -0
- package/dist/generated/prisma/models/WorksheetPreset.d.ts +1433 -0
- package/dist/generated/prisma/models/WorksheetPreset.js +1 -0
- package/dist/generated/prisma/models/WorksheetQuestion.d.ts +1491 -0
- package/dist/generated/prisma/models/WorksheetQuestion.js +1 -0
- package/dist/generated/prisma/models/WorksheetQuestionProgress.d.ts +1620 -0
- package/dist/generated/prisma/models/WorksheetQuestionProgress.js +1 -0
- package/dist/generated/prisma/models/Workspace.d.ts +3620 -0
- package/dist/generated/prisma/models/Workspace.js +1 -0
- package/dist/generated/prisma/models/WorkspaceInvitation.d.ts +1490 -0
- package/dist/generated/prisma/models/WorkspaceInvitation.js +1 -0
- package/dist/generated/prisma/models/WorkspaceKnowledgeBase.d.ts +1410 -0
- package/dist/generated/prisma/models/WorkspaceKnowledgeBase.js +1 -0
- package/dist/generated/prisma/models/WorkspaceMember.d.ts +1326 -0
- package/dist/generated/prisma/models/WorkspaceMember.js +1 -0
- package/dist/generated/prisma/models.d.ts +39 -0
- package/dist/generated/prisma/models.js +1 -0
- package/dist/src/context.d.ts +27 -0
- package/dist/src/context.js +33 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +1 -0
- package/dist/src/lib/ai/config.d.ts +20 -0
- package/dist/src/lib/ai/config.js +31 -0
- package/dist/src/lib/ai/embedding-client.d.ts +8 -0
- package/dist/src/lib/ai/embedding-client.js +30 -0
- package/dist/src/lib/ai/index.d.ts +48 -0
- package/dist/src/lib/ai/index.js +29 -0
- package/dist/src/lib/ai/inference-backend/client.d.ts +28 -0
- package/dist/src/lib/ai/inference-backend/client.js +301 -0
- package/dist/src/lib/ai/inference-backend/mocks.d.ts +12 -0
- package/dist/src/lib/ai/inference-backend/mocks.js +133 -0
- package/dist/src/lib/ai/inference-backend/types.d.ts +44 -0
- package/dist/src/lib/ai/inference-backend/types.js +1 -0
- package/dist/src/lib/ai/json-parse.d.ts +2 -0
- package/dist/src/lib/ai/json-parse.js +34 -0
- package/dist/src/lib/ai/llm-client.d.ts +7 -0
- package/dist/src/lib/ai/llm-client.js +36 -0
- package/dist/src/lib/ai/mock.d.ts +2 -0
- package/dist/src/lib/ai/mock.js +10 -0
- package/dist/src/lib/ai/types.d.ts +9 -0
- package/dist/src/lib/ai/types.js +1 -0
- package/dist/src/lib/auth.d.ts +36 -0
- package/dist/src/lib/auth.js +117 -0
- package/dist/src/lib/chunking.d.ts +19 -0
- package/dist/src/lib/chunking.js +47 -0
- package/dist/src/lib/constants.d.ts +13 -0
- package/dist/src/lib/constants.js +12 -0
- package/dist/src/lib/curated-kb-seed.d.ts +12 -0
- package/dist/src/lib/curated-kb-seed.js +155 -0
- package/dist/src/lib/email.d.ts +11 -0
- package/dist/src/lib/email.js +152 -0
- package/dist/src/lib/embeddings.d.ts +2 -0
- package/dist/src/lib/embeddings.js +1 -0
- package/dist/src/lib/ensure-curated-kb-catalog.d.ts +6 -0
- package/dist/src/lib/ensure-curated-kb-catalog.js +53 -0
- package/dist/src/lib/env.d.ts +41 -0
- package/dist/src/lib/env.js +57 -0
- package/dist/src/lib/errors.d.ts +33 -0
- package/dist/src/lib/errors.js +78 -0
- package/dist/src/lib/file.d.ts +0 -0
- package/dist/src/lib/file.js +1 -0
- package/dist/src/lib/inference.d.ts +1 -0
- package/dist/src/lib/inference.js +1 -0
- package/dist/src/lib/kb-meta.d.ts +8 -0
- package/dist/src/lib/kb-meta.js +77 -0
- package/dist/src/lib/logger.d.ts +62 -0
- package/dist/src/lib/logger.js +364 -0
- package/dist/src/lib/pdf.d.ts +11 -0
- package/dist/src/lib/pdf.js +11 -0
- package/dist/src/lib/prisma.d.ts +3 -0
- package/dist/src/lib/prisma.js +15 -0
- package/dist/src/lib/pusher.d.ts +38 -0
- package/dist/src/lib/pusher.js +170 -0
- package/dist/src/lib/retry.d.ts +15 -0
- package/dist/src/lib/retry.js +37 -0
- package/dist/src/lib/storage.d.ts +11 -0
- package/dist/src/lib/storage.js +71 -0
- package/dist/src/lib/stripe.d.ts +10 -0
- package/dist/src/lib/stripe.js +36 -0
- package/dist/src/lib/validation.d.ts +51 -0
- package/dist/src/lib/validation.js +64 -0
- package/dist/src/lib/workspace-kb.d.ts +5 -0
- package/dist/src/lib/workspace-kb.js +7 -0
- package/dist/src/repositories/artifact.repository.d.ts +64 -0
- package/dist/src/repositories/artifact.repository.js +40 -0
- package/dist/src/repositories/base.repository.d.ts +14 -0
- package/dist/src/repositories/base.repository.js +14 -0
- package/dist/src/repositories/invitation.repository.d.ts +104 -0
- package/dist/src/repositories/invitation.repository.js +44 -0
- package/dist/src/repositories/notification.repository.d.ts +76 -0
- package/dist/src/repositories/notification.repository.js +44 -0
- package/dist/src/repositories/user.repository.d.ts +84 -0
- package/dist/src/repositories/user.repository.js +37 -0
- package/dist/src/repositories/workspace-member.repository.d.ts +35 -0
- package/dist/src/repositories/workspace-member.repository.js +31 -0
- package/dist/src/repositories/workspace.repository.d.ts +101 -0
- package/dist/src/repositories/workspace.repository.js +79 -0
- package/dist/src/routers/_app.d.ts +3464 -0
- package/dist/src/routers/_app.js +36 -0
- package/dist/src/routers/admin.d.ts +358 -0
- package/dist/src/routers/admin.js +105 -0
- package/dist/src/routers/annotations.d.ts +219 -0
- package/dist/src/routers/annotations.js +29 -0
- package/dist/src/routers/artifactVersions.d.ts +65 -0
- package/dist/src/routers/artifactVersions.js +14 -0
- package/dist/src/routers/auth.d.ts +161 -0
- package/dist/src/routers/auth.js +97 -0
- package/dist/src/routers/chat.d.ts +170 -0
- package/dist/src/routers/chat.js +32 -0
- package/dist/src/routers/copilot.d.ts +200 -0
- package/dist/src/routers/copilot.js +52 -0
- package/dist/src/routers/flashcards.d.ts +336 -0
- package/dist/src/routers/flashcards.js +93 -0
- package/dist/src/routers/knowledgeBase.d.ts +421 -0
- package/dist/src/routers/knowledgeBase.js +118 -0
- package/dist/src/routers/members.d.ts +169 -0
- package/dist/src/routers/members.js +47 -0
- package/dist/src/routers/notifications.d.ts +99 -0
- package/dist/src/routers/notifications.js +25 -0
- package/dist/src/routers/payment.d.ts +80 -0
- package/dist/src/routers/payment.js +21 -0
- package/dist/src/routers/podcast.d.ts +287 -0
- package/dist/src/routers/podcast.js +34 -0
- package/dist/src/routers/studyguide.d.ts +36 -0
- package/dist/src/routers/studyguide.js +8 -0
- package/dist/src/routers/worksheets.d.ts +429 -0
- package/dist/src/routers/worksheets.js +139 -0
- package/dist/src/routers/workspace.d.ts +563 -0
- package/dist/src/routers/workspace.js +104 -0
- package/dist/src/scripts/purge-deleted-users.d.ts +1 -0
- package/dist/src/scripts/purge-deleted-users.js +148 -0
- package/dist/src/server.d.ts +1 -0
- package/dist/src/server.js +190 -0
- package/dist/src/services/activity/activity-human-description.service.d.ts +13 -0
- package/dist/src/services/activity/activity-human-description.service.js +221 -0
- package/dist/src/services/activity/activity-human-description.service.test.d.ts +1 -0
- package/dist/src/services/activity/activity-human-description.service.test.js +16 -0
- package/dist/src/services/activity/activity-log.service.d.ts +87 -0
- package/dist/src/services/activity/activity-log.service.js +276 -0
- package/dist/src/services/activity/activity-log.service.test.d.ts +1 -0
- package/dist/src/services/activity/activity-log.service.test.js +27 -0
- package/dist/src/services/admin/admin.service.d.ts +270 -0
- package/dist/src/services/admin/admin.service.js +476 -0
- package/dist/src/services/ai/ai-session.service.d.ts +5 -0
- package/dist/src/services/ai/ai-session.service.js +4 -0
- package/dist/src/services/artifacts/annotation.service.d.ts +177 -0
- package/dist/src/services/artifacts/annotation.service.js +154 -0
- package/dist/src/services/artifacts/artifact-version.service.d.ts +38 -0
- package/dist/src/services/artifacts/artifact-version.service.js +129 -0
- package/dist/src/services/artifacts/chat.service.d.ts +127 -0
- package/dist/src/services/artifacts/chat.service.js +182 -0
- package/dist/src/services/artifacts/study-guide.service.d.ts +18 -0
- package/dist/src/services/artifacts/study-guide.service.js +65 -0
- package/dist/src/services/auth/auth.service.d.ts +94 -0
- package/dist/src/services/auth/auth.service.js +368 -0
- package/dist/src/services/base.service.d.ts +14 -0
- package/dist/src/services/base.service.js +14 -0
- package/dist/src/services/billing/payment.service.d.ts +44 -0
- package/dist/src/services/billing/payment.service.js +365 -0
- package/dist/src/services/billing/subscription.service.d.ts +37 -0
- package/dist/src/services/billing/subscription.service.js +654 -0
- package/dist/src/services/billing/usage.service.d.ts +47 -0
- package/dist/src/services/billing/usage.service.js +149 -0
- package/dist/src/services/content/copilot.service.d.ts +113 -0
- package/dist/src/services/content/copilot.service.js +439 -0
- package/dist/src/services/content/flashcard-progress.service.d.ts +159 -0
- package/dist/src/services/content/flashcard-progress.service.js +432 -0
- package/dist/src/services/content/flashcard.service.d.ts +184 -0
- package/dist/src/services/content/flashcard.service.js +339 -0
- package/dist/src/services/content/media-analysis.service.d.ts +23 -0
- package/dist/src/services/content/media-analysis.service.js +404 -0
- package/dist/src/services/content/podcast.service.d.ts +267 -0
- package/dist/src/services/content/podcast.service.js +653 -0
- package/dist/src/services/content/worksheet-content.service.d.ts +37 -0
- package/dist/src/services/content/worksheet-content.service.js +84 -0
- package/dist/src/services/content/worksheet-content.service.test.d.ts +1 -0
- package/dist/src/services/content/worksheet-content.service.test.js +69 -0
- package/dist/src/services/content/worksheet-generation.service.d.ts +91 -0
- package/dist/src/services/content/worksheet-generation.service.js +95 -0
- package/dist/src/services/content/worksheet-generation.service.test.d.ts +1 -0
- package/dist/src/services/content/worksheet-generation.service.test.js +20 -0
- package/dist/src/services/content/worksheet.service.d.ts +347 -0
- package/dist/src/services/content/worksheet.service.js +599 -0
- package/dist/src/services/knowledge/knowledge-base.service.d.ts +316 -0
- package/dist/src/services/knowledge/knowledge-base.service.js +544 -0
- package/dist/src/services/members/invitation.service.d.ts +66 -0
- package/dist/src/services/members/invitation.service.js +348 -0
- package/dist/src/services/members/member.service.d.ts +36 -0
- package/dist/src/services/members/member.service.js +193 -0
- package/dist/src/services/notifications/notification.service.d.ts +214 -0
- package/dist/src/services/notifications/notification.service.js +550 -0
- package/dist/src/services/notifications/notification.service.test.d.ts +1 -0
- package/dist/src/services/notifications/notification.service.test.js +87 -0
- package/dist/src/services/workspace/workspace-analytics.service.d.ts +24 -0
- package/dist/src/services/workspace/workspace-analytics.service.js +95 -0
- package/dist/src/services/workspace/workspace-kb.service.d.ts +40 -0
- package/dist/src/services/workspace/workspace-kb.service.js +184 -0
- package/dist/src/services/workspace/workspace.service.d.ts +263 -0
- package/dist/src/services/workspace/workspace.service.js +401 -0
- package/dist/src/trpc.d.ts +60 -0
- package/dist/src/trpc.js +217 -0
- package/dist/src/types/index.d.ts +126 -0
- package/dist/src/types/index.js +1 -0
- package/package.json +8 -9
- package/prisma/schema.prisma +3 -4
- package/prisma/seed.mjs +5 -2
- package/prisma.config.ts +16 -0
- package/src/lib/prisma.ts +18 -9
- package/src/scripts/purge-deleted-users.ts +1 -3
- package/tsconfig.json +3 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { TRPCError } from '@trpc/server';
|
|
2
|
+
import { BaseService } from '../base.service.js';
|
|
3
|
+
import { supabaseClient } from '../../lib/storage.js';
|
|
4
|
+
import PusherService from '../../lib/pusher.js';
|
|
5
|
+
import { ai } from '../../lib/ai/index.js';
|
|
6
|
+
import { workspaceKbService } from './workspace-kb.service.js';
|
|
7
|
+
import { getAccountSummary, invalidateUserBillingCache, } from '../billing/usage.service.js';
|
|
8
|
+
import { getUserStorageLimit } from '../billing/subscription.service.js';
|
|
9
|
+
import { notifyWorkspaceDeleted } from '../notifications/notification.service.js';
|
|
10
|
+
export class WorkspaceService extends BaseService {
|
|
11
|
+
constructor(db) {
|
|
12
|
+
super(db);
|
|
13
|
+
}
|
|
14
|
+
async list(userId, parentId) {
|
|
15
|
+
const workspaces = await this.db.workspace.findMany({
|
|
16
|
+
where: { ownerId: userId, folderId: parentId },
|
|
17
|
+
orderBy: { updatedAt: 'desc' },
|
|
18
|
+
});
|
|
19
|
+
const folders = await this.db.folder.findMany({
|
|
20
|
+
where: { ownerId: userId, parentId },
|
|
21
|
+
});
|
|
22
|
+
return { workspaces, folders };
|
|
23
|
+
}
|
|
24
|
+
async getTree(userId) {
|
|
25
|
+
const [allFolders, allWorkspaces] = await Promise.all([
|
|
26
|
+
this.db.folder.findMany({
|
|
27
|
+
where: { ownerId: userId },
|
|
28
|
+
select: {
|
|
29
|
+
id: true,
|
|
30
|
+
name: true,
|
|
31
|
+
parentId: true,
|
|
32
|
+
color: true,
|
|
33
|
+
markerColor: true,
|
|
34
|
+
updatedAt: true,
|
|
35
|
+
},
|
|
36
|
+
orderBy: { updatedAt: 'desc' },
|
|
37
|
+
}),
|
|
38
|
+
this.db.workspace.findMany({
|
|
39
|
+
where: { ownerId: userId },
|
|
40
|
+
select: {
|
|
41
|
+
id: true,
|
|
42
|
+
title: true,
|
|
43
|
+
folderId: true,
|
|
44
|
+
icon: true,
|
|
45
|
+
color: true,
|
|
46
|
+
markerColor: true,
|
|
47
|
+
updatedAt: true,
|
|
48
|
+
uploads: {
|
|
49
|
+
select: {
|
|
50
|
+
id: true,
|
|
51
|
+
name: true,
|
|
52
|
+
mimeType: true,
|
|
53
|
+
createdAt: true,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
orderBy: { updatedAt: 'desc' },
|
|
58
|
+
}),
|
|
59
|
+
]);
|
|
60
|
+
return { folders: allFolders, workspaces: allWorkspaces };
|
|
61
|
+
}
|
|
62
|
+
async create(userId, input) {
|
|
63
|
+
const ws = await this.db.workspace.create({
|
|
64
|
+
data: {
|
|
65
|
+
title: input.name,
|
|
66
|
+
description: input.description,
|
|
67
|
+
ownerId: userId,
|
|
68
|
+
folderId: input.parentId ?? null,
|
|
69
|
+
...(input.markerColor !== undefined ? { markerColor: input.markerColor } : {}),
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
void ai.backend.initSession(ws.id, userId).catch((err) => {
|
|
73
|
+
this.logger.error('Failed to init AI session on workspace creation:', err);
|
|
74
|
+
});
|
|
75
|
+
void workspaceKbService.ensureWorkspaceKb(ws.id, userId, input.name).catch((err) => {
|
|
76
|
+
this.logger.error('Failed to create workspace knowledge base:', err);
|
|
77
|
+
});
|
|
78
|
+
invalidateUserBillingCache(userId);
|
|
79
|
+
void PusherService.emitLibraryUpdate(userId);
|
|
80
|
+
return ws;
|
|
81
|
+
}
|
|
82
|
+
async createFolder(userId, input) {
|
|
83
|
+
const folder = await this.db.folder.create({
|
|
84
|
+
data: {
|
|
85
|
+
name: input.name,
|
|
86
|
+
ownerId: userId,
|
|
87
|
+
color: input.color ?? '#9D00FF',
|
|
88
|
+
parentId: input.parentId ?? null,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
await PusherService.emitLibraryUpdate(userId);
|
|
92
|
+
return folder;
|
|
93
|
+
}
|
|
94
|
+
async updateFolder(userId, input) {
|
|
95
|
+
const folder = await this.db.folder.update({
|
|
96
|
+
where: { id: input.id },
|
|
97
|
+
data: { name: input.name, markerColor: input.markerColor },
|
|
98
|
+
});
|
|
99
|
+
await PusherService.emitLibraryUpdate(userId);
|
|
100
|
+
return folder;
|
|
101
|
+
}
|
|
102
|
+
async deleteFolder(userId, id) {
|
|
103
|
+
const folder = await this.db.folder.delete({ where: { id } });
|
|
104
|
+
await PusherService.emitLibraryUpdate(userId);
|
|
105
|
+
return folder;
|
|
106
|
+
}
|
|
107
|
+
async get(userId, id) {
|
|
108
|
+
const ws = await this.db.workspace.findFirst({
|
|
109
|
+
where: { id, ownerId: userId },
|
|
110
|
+
select: {
|
|
111
|
+
id: true,
|
|
112
|
+
title: true,
|
|
113
|
+
description: true,
|
|
114
|
+
icon: true,
|
|
115
|
+
color: true,
|
|
116
|
+
markerColor: true,
|
|
117
|
+
ownerId: true,
|
|
118
|
+
folderId: true,
|
|
119
|
+
fileBeingAnalyzed: true,
|
|
120
|
+
analysisProgress: true,
|
|
121
|
+
needsAnalysis: true,
|
|
122
|
+
createdAt: true,
|
|
123
|
+
updatedAt: true,
|
|
124
|
+
folder: { select: { id: true, name: true, color: true } },
|
|
125
|
+
uploads: {
|
|
126
|
+
select: {
|
|
127
|
+
id: true,
|
|
128
|
+
name: true,
|
|
129
|
+
mimeType: true,
|
|
130
|
+
size: true,
|
|
131
|
+
createdAt: true,
|
|
132
|
+
objectKey: true,
|
|
133
|
+
},
|
|
134
|
+
orderBy: { createdAt: 'desc' },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
if (!ws)
|
|
139
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
140
|
+
return ws;
|
|
141
|
+
}
|
|
142
|
+
async getAccountSummary(userId) {
|
|
143
|
+
return getAccountSummary(userId);
|
|
144
|
+
}
|
|
145
|
+
async getStats(userId) {
|
|
146
|
+
const summary = await getAccountSummary(userId);
|
|
147
|
+
return summary.stats;
|
|
148
|
+
}
|
|
149
|
+
async update(userId, input) {
|
|
150
|
+
const existed = await this.db.workspace.findFirst({
|
|
151
|
+
where: { id: input.id, ownerId: userId },
|
|
152
|
+
});
|
|
153
|
+
if (!existed)
|
|
154
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
155
|
+
const updated = await this.db.workspace.update({
|
|
156
|
+
where: { id: input.id },
|
|
157
|
+
data: {
|
|
158
|
+
title: input.name ?? existed.title,
|
|
159
|
+
description: input.description,
|
|
160
|
+
markerColor: input.markerColor !== undefined ? input.markerColor : existed.markerColor,
|
|
161
|
+
icon: input.icon ?? existed.icon,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
await PusherService.emitLibraryUpdate(userId);
|
|
165
|
+
return updated;
|
|
166
|
+
}
|
|
167
|
+
async delete(userId, id) {
|
|
168
|
+
const workspaceToDelete = await this.db.workspace.findFirst({
|
|
169
|
+
where: { id, ownerId: userId },
|
|
170
|
+
select: {
|
|
171
|
+
id: true,
|
|
172
|
+
title: true,
|
|
173
|
+
ownerId: true,
|
|
174
|
+
members: { select: { userId: true } },
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
if (!workspaceToDelete)
|
|
178
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
179
|
+
const actor = await this.db.user.findUnique({
|
|
180
|
+
where: { id: userId },
|
|
181
|
+
select: { name: true, email: true },
|
|
182
|
+
});
|
|
183
|
+
const actorName = actor?.name || actor?.email || 'A user';
|
|
184
|
+
await notifyWorkspaceDeleted(this.db, {
|
|
185
|
+
recipientUserIds: workspaceToDelete.members.map((m) => m.userId),
|
|
186
|
+
actorUserId: userId,
|
|
187
|
+
actorName,
|
|
188
|
+
workspaceId: workspaceToDelete.id,
|
|
189
|
+
workspaceTitle: workspaceToDelete.title,
|
|
190
|
+
});
|
|
191
|
+
const deleted = await this.db.workspace.deleteMany({
|
|
192
|
+
where: { id, ownerId: userId },
|
|
193
|
+
});
|
|
194
|
+
if (deleted.count === 0)
|
|
195
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
196
|
+
await PusherService.emitLibraryUpdate(userId);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
async getFolderInformation(userId, id) {
|
|
200
|
+
const folder = await this.db.folder.findFirst({
|
|
201
|
+
where: { id, ownerId: userId },
|
|
202
|
+
});
|
|
203
|
+
if (!folder)
|
|
204
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
205
|
+
const parents = [];
|
|
206
|
+
let current = folder;
|
|
207
|
+
while (current.parentId) {
|
|
208
|
+
const parent = await this.db.folder.findFirst({
|
|
209
|
+
where: { id: current.parentId, ownerId: userId },
|
|
210
|
+
});
|
|
211
|
+
if (!parent)
|
|
212
|
+
break;
|
|
213
|
+
parents.push(parent);
|
|
214
|
+
current = parent;
|
|
215
|
+
}
|
|
216
|
+
return { folder, parents };
|
|
217
|
+
}
|
|
218
|
+
async getSharedWith(userId, id) {
|
|
219
|
+
const user = await this.db.user.findFirst({ where: { id: userId } });
|
|
220
|
+
if (!user || !user.email)
|
|
221
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
222
|
+
const sharedWith = await this.db.workspace.findMany({
|
|
223
|
+
where: { members: { some: { userId } } },
|
|
224
|
+
});
|
|
225
|
+
const invitations = await this.db.workspaceInvitation.findMany({
|
|
226
|
+
where: { email: user.email, acceptedAt: null },
|
|
227
|
+
include: { workspace: true },
|
|
228
|
+
});
|
|
229
|
+
return { shared: sharedWith, invitations };
|
|
230
|
+
}
|
|
231
|
+
async uploadFiles(userId, input) {
|
|
232
|
+
const ws = await this.db.workspace.findFirst({
|
|
233
|
+
where: { id: input.id, ownerId: userId },
|
|
234
|
+
});
|
|
235
|
+
if (!ws)
|
|
236
|
+
throw new TRPCError({ code: 'NOT_FOUND' });
|
|
237
|
+
const workspaces = await this.db.workspace.findMany({
|
|
238
|
+
where: {
|
|
239
|
+
OR: [{ ownerId: userId }, { sharedWith: { some: { id: userId } } }],
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
const spaceUsed = await this.db.fileAsset.aggregate({
|
|
243
|
+
where: { workspaceId: { in: workspaces.map((w) => w.id) }, userId },
|
|
244
|
+
_sum: { size: true },
|
|
245
|
+
});
|
|
246
|
+
const storageLimit = await getUserStorageLimit(userId);
|
|
247
|
+
const totalSize = input.files.reduce((acc, file) => acc + file.size, 0);
|
|
248
|
+
if ((spaceUsed._sum?.size ?? 0) + totalSize > storageLimit) {
|
|
249
|
+
this.logger.warn(`Storage limit exceeded for user ${userId}. Used: ${spaceUsed._sum?.size}, Tried to upload: ${totalSize}, Limit: ${storageLimit}`);
|
|
250
|
+
throw new TRPCError({
|
|
251
|
+
code: 'FORBIDDEN',
|
|
252
|
+
message: `Storage limit exceeded. Maximum allowed storage is ${(storageLimit / (1024 * 1024 * 1024)).toFixed(1)}GB.`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const results = [];
|
|
256
|
+
for (const file of input.files) {
|
|
257
|
+
const record = await this.db.fileAsset.create({
|
|
258
|
+
data: {
|
|
259
|
+
userId,
|
|
260
|
+
name: file.filename,
|
|
261
|
+
mimeType: file.contentType,
|
|
262
|
+
size: file.size,
|
|
263
|
+
workspaceId: input.id,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
const objectKey = `${userId}/${record.id}-${file.filename}`;
|
|
267
|
+
const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
|
|
268
|
+
.from('media')
|
|
269
|
+
.createSignedUploadUrl(objectKey);
|
|
270
|
+
if (signedUrlError) {
|
|
271
|
+
throw new TRPCError({
|
|
272
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
273
|
+
message: `Failed to upload file`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
await this.db.fileAsset.update({
|
|
277
|
+
where: { id: record.id },
|
|
278
|
+
data: {
|
|
279
|
+
bucket: 'media',
|
|
280
|
+
objectKey,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
results.push({
|
|
284
|
+
fileId: record.id,
|
|
285
|
+
uploadUrl: signedUrlData.signedUrl,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return results;
|
|
289
|
+
}
|
|
290
|
+
async deleteFiles(userId, input) {
|
|
291
|
+
const files = await this.db.fileAsset.findMany({
|
|
292
|
+
where: {
|
|
293
|
+
id: { in: input.fileId },
|
|
294
|
+
workspaceId: input.id,
|
|
295
|
+
userId,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
for (const file of files) {
|
|
299
|
+
if (file.bucket && file.objectKey) {
|
|
300
|
+
supabaseClient.storage
|
|
301
|
+
.from(file.bucket)
|
|
302
|
+
.remove([file.objectKey])
|
|
303
|
+
.catch((err) => {
|
|
304
|
+
this.logger.error(`Error deleting file ${file.objectKey} from bucket ${file.bucket}:`, err);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
await this.db.fileAsset.deleteMany({
|
|
309
|
+
where: {
|
|
310
|
+
id: { in: input.fileId },
|
|
311
|
+
workspaceId: input.id,
|
|
312
|
+
userId,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
async getFileUploadUrl(userId, input) {
|
|
318
|
+
const workspaces = await this.db.workspace.findMany({
|
|
319
|
+
where: {
|
|
320
|
+
OR: [{ ownerId: userId }, { sharedWith: { some: { id: userId } } }],
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
const spaceUsed = await this.db.fileAsset.aggregate({
|
|
324
|
+
where: { workspaceId: { in: workspaces.map((w) => w.id) }, userId },
|
|
325
|
+
_sum: { size: true },
|
|
326
|
+
});
|
|
327
|
+
const storageLimit = await getUserStorageLimit(userId);
|
|
328
|
+
if ((spaceUsed._sum?.size ?? 0) + input.size > storageLimit) {
|
|
329
|
+
this.logger.warn(`Storage limit exceeded for user ${userId}. Used: ${spaceUsed._sum?.size}, Tried to upload: ${input.size}, Limit: ${storageLimit}`);
|
|
330
|
+
throw new TRPCError({
|
|
331
|
+
code: 'FORBIDDEN',
|
|
332
|
+
message: `Storage limit exceeded. Maximum allowed storage is ${(storageLimit / (1024 * 1024 * 1024)).toFixed(1)}GB.`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const objectKey = `workspace_${userId}/${input.workspaceId}-file_${input.filename}`;
|
|
336
|
+
const fileAsset = await this.db.fileAsset.create({
|
|
337
|
+
data: {
|
|
338
|
+
workspaceId: input.workspaceId,
|
|
339
|
+
name: input.filename,
|
|
340
|
+
mimeType: input.contentType,
|
|
341
|
+
size: input.size,
|
|
342
|
+
userId,
|
|
343
|
+
bucket: 'media',
|
|
344
|
+
objectKey,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
const { data: signedUrlData, error: signedUrlError } = await supabaseClient.storage
|
|
348
|
+
.from('media')
|
|
349
|
+
.createSignedUploadUrl(objectKey, { upsert: true });
|
|
350
|
+
if (signedUrlError) {
|
|
351
|
+
this.logger.error('Signed upload URL error:', signedUrlError);
|
|
352
|
+
throw new TRPCError({
|
|
353
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
354
|
+
message: `Failed to create upload URL: ${signedUrlError.message}`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
await this.db.workspace.update({
|
|
358
|
+
where: { id: input.workspaceId },
|
|
359
|
+
data: { needsAnalysis: true },
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
fileId: fileAsset.id,
|
|
363
|
+
uploadUrl: signedUrlData.signedUrl,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
async search(userId, input) {
|
|
367
|
+
const { query, color } = input;
|
|
368
|
+
const workspaces = await this.db.workspace.findMany({
|
|
369
|
+
where: {
|
|
370
|
+
ownerId: userId,
|
|
371
|
+
markerColor: color || undefined,
|
|
372
|
+
...(query
|
|
373
|
+
? {
|
|
374
|
+
OR: [
|
|
375
|
+
{ title: { contains: query, mode: 'insensitive' } },
|
|
376
|
+
{ description: { contains: query, mode: 'insensitive' } },
|
|
377
|
+
],
|
|
378
|
+
}
|
|
379
|
+
: {}),
|
|
380
|
+
},
|
|
381
|
+
orderBy: { updatedAt: 'desc' },
|
|
382
|
+
take: input.limit,
|
|
383
|
+
});
|
|
384
|
+
const folders = await this.db.folder.findMany({
|
|
385
|
+
where: {
|
|
386
|
+
ownerId: userId,
|
|
387
|
+
markerColor: color || undefined,
|
|
388
|
+
...(query ? { name: { contains: query, mode: 'insensitive' } } : {}),
|
|
389
|
+
},
|
|
390
|
+
orderBy: { updatedAt: 'desc' },
|
|
391
|
+
take: input.limit,
|
|
392
|
+
});
|
|
393
|
+
const results = [
|
|
394
|
+
...workspaces.map((w) => ({ ...w, type: 'workspace' })),
|
|
395
|
+
...folders.map((f) => ({ ...f, type: 'folder', title: f.name })),
|
|
396
|
+
]
|
|
397
|
+
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
|
|
398
|
+
.slice(0, input.limit);
|
|
399
|
+
return results;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Context } from "./context.js";
|
|
2
|
+
export declare const router: import("@trpc/server").TRPCRouterBuilder<{
|
|
3
|
+
ctx: Context;
|
|
4
|
+
meta: object;
|
|
5
|
+
errorShape: {
|
|
6
|
+
data: {
|
|
7
|
+
zodError: string | null;
|
|
8
|
+
code: import("@trpc/server").TRPC_ERROR_CODE_KEY;
|
|
9
|
+
httpStatus: number;
|
|
10
|
+
path?: string;
|
|
11
|
+
stack?: string;
|
|
12
|
+
};
|
|
13
|
+
message: string;
|
|
14
|
+
code: import("@trpc/server").TRPC_ERROR_CODE_NUMBER;
|
|
15
|
+
};
|
|
16
|
+
transformer: true;
|
|
17
|
+
}>;
|
|
18
|
+
export declare const middleware: <$ContextOverrides>(fn: import("@trpc/server").TRPCMiddlewareFunction<Context, object, object, $ContextOverrides, unknown>) => import("@trpc/server").TRPCMiddlewareBuilder<Context, object, $ContextOverrides, unknown>;
|
|
19
|
+
export declare const publicProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, object, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
|
|
20
|
+
/** Exported procedures with middleware */
|
|
21
|
+
export declare const authedProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
|
|
22
|
+
userId: any;
|
|
23
|
+
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
24
|
+
res: import("express").Response<any, Record<string, any>>;
|
|
25
|
+
session: {
|
|
26
|
+
user: import("./context.js").SessionUser;
|
|
27
|
+
};
|
|
28
|
+
db: import("@prisma/client").PrismaClient;
|
|
29
|
+
cookies: import("cookie").Cookies;
|
|
30
|
+
}, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
|
|
31
|
+
export declare const verifiedProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
|
|
32
|
+
userId: any;
|
|
33
|
+
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
34
|
+
res: import("express").Response<any, Record<string, any>>;
|
|
35
|
+
session: {
|
|
36
|
+
user: import("./context.js").SessionUser;
|
|
37
|
+
};
|
|
38
|
+
db: import("@prisma/client").PrismaClient;
|
|
39
|
+
cookies: import("cookie").Cookies;
|
|
40
|
+
}, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
|
|
41
|
+
export declare const adminProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
|
|
42
|
+
userId: any;
|
|
43
|
+
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
44
|
+
res: import("express").Response<any, Record<string, any>>;
|
|
45
|
+
session: {
|
|
46
|
+
user: import("./context.js").SessionUser;
|
|
47
|
+
};
|
|
48
|
+
db: import("@prisma/client").PrismaClient;
|
|
49
|
+
cookies: import("cookie").Cookies;
|
|
50
|
+
}, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
|
|
51
|
+
export declare const limitedProcedure: import("@trpc/server").TRPCProcedureBuilder<Context, object, {
|
|
52
|
+
userId: any;
|
|
53
|
+
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
54
|
+
res: import("express").Response<any, Record<string, any>>;
|
|
55
|
+
session: {
|
|
56
|
+
user: import("./context.js").SessionUser;
|
|
57
|
+
};
|
|
58
|
+
db: import("@prisma/client").PrismaClient;
|
|
59
|
+
cookies: import("cookie").Cookies;
|
|
60
|
+
}, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, import("@trpc/server").TRPCUnsetMarker, false>;
|
package/dist/src/trpc.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { initTRPC, TRPCError } from "@trpc/server";
|
|
2
|
+
import superjson from "superjson";
|
|
3
|
+
import { ActivityLogStatus } from "@prisma/client";
|
|
4
|
+
import { logger } from "./lib/logger.js";
|
|
5
|
+
import { toTRPCError } from "./lib/errors.js";
|
|
6
|
+
import { getUserUsage, getUserPlanLimits } from "./services/billing/usage.service.js";
|
|
7
|
+
import { getClientIp, isActivityLogEnabled, scheduleRecordActivity, truncateUserAgent, } from "./services/activity/activity-log.service.js";
|
|
8
|
+
/** Avoid logging the log viewers themselves (noise when browsing activity). */
|
|
9
|
+
const SKIP_ACTIVITY_TRPC_PATHS = new Set([
|
|
10
|
+
"admin.activityList",
|
|
11
|
+
"admin.activityExportCsv",
|
|
12
|
+
]);
|
|
13
|
+
const t = initTRPC.context().create({
|
|
14
|
+
transformer: superjson,
|
|
15
|
+
errorFormatter({ shape, error }) {
|
|
16
|
+
// Log errors in development
|
|
17
|
+
if (process.env.NODE_ENV === 'development') {
|
|
18
|
+
logger.error('TRPC Error', 'TRPC', {
|
|
19
|
+
code: error.code,
|
|
20
|
+
message: error.message,
|
|
21
|
+
cause: error.cause,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
...shape,
|
|
26
|
+
data: {
|
|
27
|
+
...shape.data,
|
|
28
|
+
zodError: error.cause instanceof Error ? error.cause.message : null,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
export const router = t.router;
|
|
34
|
+
export const middleware = t.middleware;
|
|
35
|
+
export const publicProcedure = t.procedure;
|
|
36
|
+
/**
|
|
37
|
+
* Logging middleware
|
|
38
|
+
*/
|
|
39
|
+
const loggingMiddleware = middleware(async ({ ctx, next, path, type }) => {
|
|
40
|
+
const start = Date.now();
|
|
41
|
+
const result = await next();
|
|
42
|
+
const duration = Date.now() - start;
|
|
43
|
+
logger.info(`TRPC ${type} ${path}`, 'TRPC', {
|
|
44
|
+
duration: `${duration}ms`,
|
|
45
|
+
userId: ctx.session?.user?.id,
|
|
46
|
+
});
|
|
47
|
+
return result;
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* Middleware that enforces authentication
|
|
51
|
+
*/
|
|
52
|
+
const isAuthed = middleware(({ ctx, next }) => {
|
|
53
|
+
const hasUser = Boolean(ctx.session?.user?.id);
|
|
54
|
+
if (!ctx.session || !hasUser) {
|
|
55
|
+
throw new TRPCError({
|
|
56
|
+
code: "UNAUTHORIZED",
|
|
57
|
+
message: "You must be logged in to access this resource"
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return next({
|
|
61
|
+
ctx: {
|
|
62
|
+
...ctx,
|
|
63
|
+
session: ctx.session,
|
|
64
|
+
userId: ctx.session.user.id,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* Error handling middleware
|
|
70
|
+
*/
|
|
71
|
+
const errorHandler = middleware(async ({ next }) => {
|
|
72
|
+
try {
|
|
73
|
+
return await next();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw toTRPCError(error);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
/**
|
|
80
|
+
* Middleware that enforces email verification
|
|
81
|
+
*/
|
|
82
|
+
const isVerified = middleware(({ ctx, next }) => {
|
|
83
|
+
const user = ctx.session?.user;
|
|
84
|
+
if (!user?.emailVerified) {
|
|
85
|
+
throw new TRPCError({
|
|
86
|
+
code: "FORBIDDEN",
|
|
87
|
+
message: "Please verify your email to access this feature",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return next();
|
|
91
|
+
});
|
|
92
|
+
/**
|
|
93
|
+
* Middleware that enforces resource limits based on the user's plan.
|
|
94
|
+
* Note: This matches the 'path' to decide which limit to check.
|
|
95
|
+
*/
|
|
96
|
+
const checkUsageLimit = middleware(async ({ ctx, next, path }) => {
|
|
97
|
+
const userId = ctx.session.user.id;
|
|
98
|
+
// 1. Get current usage and limits
|
|
99
|
+
const [usage, limits] = await Promise.all([
|
|
100
|
+
getUserUsage(userId),
|
|
101
|
+
getUserPlanLimits(userId)
|
|
102
|
+
]);
|
|
103
|
+
// If no limits found (no active plan), we block any premium actions
|
|
104
|
+
if (!limits) {
|
|
105
|
+
throw new TRPCError({
|
|
106
|
+
code: "FORBIDDEN",
|
|
107
|
+
message: "No active plan found. Please subscribe to a plan to continue.",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// 2. Check limits based on the tRPC path
|
|
111
|
+
// Flashcards (matches: flashcards.createCard, flashcards.generateFromPrompt)
|
|
112
|
+
if (path.startsWith('flashcards.') && (path.includes('create') || path.includes('generate')) && usage.flashcards >= limits.maxFlashcards) {
|
|
113
|
+
throw new TRPCError({ code: "FORBIDDEN", message: "Flashcard limit reached for your plan." });
|
|
114
|
+
}
|
|
115
|
+
// Worksheets (matches: worksheets.create, worksheets.generateFromPrompt)
|
|
116
|
+
if (path.startsWith('worksheets.') && (path.includes('create') || path.includes('generate')) && usage.worksheets >= limits.maxWorksheets) {
|
|
117
|
+
throw new TRPCError({ code: "FORBIDDEN", message: "Worksheet limit reached for your plan." });
|
|
118
|
+
}
|
|
119
|
+
// Podcasts (matches: podcast.generateEpisode)
|
|
120
|
+
if (path.startsWith('podcast.') && path.includes('generate') && usage.podcasts >= limits.maxPodcasts) {
|
|
121
|
+
throw new TRPCError({ code: "FORBIDDEN", message: "Podcast limit reached for your plan." });
|
|
122
|
+
}
|
|
123
|
+
// Study Guides (matches: studyguide.get - because it lazily creates)
|
|
124
|
+
if (path.startsWith('studyguide.') && path.includes('get') && usage.studyGuides >= limits.maxStudyGuides) {
|
|
125
|
+
// Note: We only block if the artifact doesn't exist yet (handled inside the query or by checking usage)
|
|
126
|
+
// For simplicity, if they hit the limit, we block creation of NEW study guides.
|
|
127
|
+
// However, usage_service counts existing ones.
|
|
128
|
+
// If usage is already at/over limit, it means any NEW workspace's 'get' will fail creation.
|
|
129
|
+
}
|
|
130
|
+
// Storage check (for uploads)
|
|
131
|
+
if (path.includes('upload') && usage.storageBytes >= Number(limits.maxStorageBytes)) {
|
|
132
|
+
throw new TRPCError({ code: "FORBIDDEN", message: "Storage limit reached for your plan." });
|
|
133
|
+
}
|
|
134
|
+
return next();
|
|
135
|
+
});
|
|
136
|
+
/**
|
|
137
|
+
* Middleware that enforces system admin role
|
|
138
|
+
*/
|
|
139
|
+
const isAdmin = middleware(({ ctx, next }) => {
|
|
140
|
+
const user = ctx.session?.user;
|
|
141
|
+
if (!user?.isSystemAdmin) {
|
|
142
|
+
throw new TRPCError({
|
|
143
|
+
code: "FORBIDDEN",
|
|
144
|
+
message: "You do not have permission to access this resource",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return next();
|
|
148
|
+
});
|
|
149
|
+
/**
|
|
150
|
+
* Persists ActivityLog rows for authenticated tRPC calls (async, non-blocking).
|
|
151
|
+
*/
|
|
152
|
+
const activityLogMiddleware = middleware(async (opts) => {
|
|
153
|
+
const { ctx, next, path, type, getRawInput } = opts;
|
|
154
|
+
if (!isActivityLogEnabled() || SKIP_ACTIVITY_TRPC_PATHS.has(path)) {
|
|
155
|
+
return next();
|
|
156
|
+
}
|
|
157
|
+
const userId = ctx.userId;
|
|
158
|
+
if (!userId) {
|
|
159
|
+
return next();
|
|
160
|
+
}
|
|
161
|
+
let rawInput;
|
|
162
|
+
try {
|
|
163
|
+
rawInput = await getRawInput();
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
rawInput = undefined;
|
|
167
|
+
}
|
|
168
|
+
const req = ctx.req;
|
|
169
|
+
const ipAddress = req ? getClientIp(req) : undefined;
|
|
170
|
+
const userAgent = req.headers["user-agent"];
|
|
171
|
+
const httpMethod = req.method;
|
|
172
|
+
const start = Date.now();
|
|
173
|
+
const result = await next();
|
|
174
|
+
const durationMs = Date.now() - start;
|
|
175
|
+
if (result.ok) {
|
|
176
|
+
scheduleRecordActivity({
|
|
177
|
+
db: ctx.db,
|
|
178
|
+
actorUserId: userId,
|
|
179
|
+
path,
|
|
180
|
+
type,
|
|
181
|
+
status: ActivityLogStatus.SUCCESS,
|
|
182
|
+
durationMs,
|
|
183
|
+
rawInput,
|
|
184
|
+
ipAddress,
|
|
185
|
+
userAgent: truncateUserAgent(typeof userAgent === "string" ? userAgent : undefined),
|
|
186
|
+
httpMethod,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
scheduleRecordActivity({
|
|
191
|
+
db: ctx.db,
|
|
192
|
+
actorUserId: userId,
|
|
193
|
+
path,
|
|
194
|
+
type,
|
|
195
|
+
status: ActivityLogStatus.FAILURE,
|
|
196
|
+
durationMs,
|
|
197
|
+
rawInput,
|
|
198
|
+
ipAddress,
|
|
199
|
+
userAgent: truncateUserAgent(typeof userAgent === "string" ? userAgent : undefined),
|
|
200
|
+
httpMethod,
|
|
201
|
+
errorCode: result.error.code,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
});
|
|
206
|
+
/** Exported procedures with middleware */
|
|
207
|
+
export const authedProcedure = publicProcedure
|
|
208
|
+
.use(loggingMiddleware)
|
|
209
|
+
.use(errorHandler)
|
|
210
|
+
.use(isAuthed)
|
|
211
|
+
.use(activityLogMiddleware);
|
|
212
|
+
export const verifiedProcedure = authedProcedure
|
|
213
|
+
.use(isVerified);
|
|
214
|
+
export const adminProcedure = authedProcedure
|
|
215
|
+
.use(isAdmin);
|
|
216
|
+
export const limitedProcedure = verifiedProcedure
|
|
217
|
+
.use(checkUsageLimit);
|