@studious-lms/server 1.2.45 → 1.2.47

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.
Files changed (241) hide show
  1. package/.env.example +45 -0
  2. package/.env.test.example +37 -0
  3. package/README.md +34 -7
  4. package/coverage/base.css +224 -0
  5. package/coverage/block-navigation.js +87 -0
  6. package/coverage/clover.xml +12110 -0
  7. package/coverage/coverage-final.json +44 -0
  8. package/coverage/favicon.png +0 -0
  9. package/coverage/index.html +221 -0
  10. package/coverage/prettify.css +1 -0
  11. package/coverage/prettify.js +2 -0
  12. package/coverage/server/index.html +116 -0
  13. package/coverage/server/src/exportType.ts.html +109 -0
  14. package/coverage/server/src/index.html +161 -0
  15. package/coverage/server/src/index.ts.html +1702 -0
  16. package/coverage/server/src/instrument.ts.html +130 -0
  17. package/coverage/server/src/lib/config/env.ts.html +448 -0
  18. package/coverage/server/src/lib/config/index.html +116 -0
  19. package/coverage/server/src/lib/fileUpload.ts.html +1138 -0
  20. package/coverage/server/src/lib/googleCloudStorage.ts.html +334 -0
  21. package/coverage/server/src/lib/index.html +206 -0
  22. package/coverage/server/src/lib/jsonConversion.ts.html +2323 -0
  23. package/coverage/server/src/lib/jsonStyles.ts.html +193 -0
  24. package/coverage/server/src/lib/notificationHandler.ts.html +193 -0
  25. package/coverage/server/src/lib/pusher.ts.html +121 -0
  26. package/coverage/server/src/lib/thumbnailGenerator.ts.html +592 -0
  27. package/coverage/server/src/middleware/auth.ts.html +646 -0
  28. package/coverage/server/src/middleware/index.html +146 -0
  29. package/coverage/server/src/middleware/logging.ts.html +244 -0
  30. package/coverage/server/src/middleware/security.ts.html +271 -0
  31. package/coverage/server/src/routers/_app.ts.html +232 -0
  32. package/coverage/server/src/routers/agenda.ts.html +319 -0
  33. package/coverage/server/src/routers/announcement.ts.html +3481 -0
  34. package/coverage/server/src/routers/assignment.ts.html +7633 -0
  35. package/coverage/server/src/routers/attendance.ts.html +1030 -0
  36. package/coverage/server/src/routers/auth.ts.html +1081 -0
  37. package/coverage/server/src/routers/class.ts.html +3535 -0
  38. package/coverage/server/src/routers/comment.ts.html +991 -0
  39. package/coverage/server/src/routers/conversation.ts.html +982 -0
  40. package/coverage/server/src/routers/event.ts.html +1609 -0
  41. package/coverage/server/src/routers/file.ts.html +1144 -0
  42. package/coverage/server/src/routers/folder.ts.html +2797 -0
  43. package/coverage/server/src/routers/index.html +386 -0
  44. package/coverage/server/src/routers/labChat.ts.html +3073 -0
  45. package/coverage/server/src/routers/marketing.ts.html +340 -0
  46. package/coverage/server/src/routers/message.ts.html +1912 -0
  47. package/coverage/server/src/routers/notifications.ts.html +364 -0
  48. package/coverage/server/src/routers/section.ts.html +1120 -0
  49. package/coverage/server/src/routers/user.ts.html +862 -0
  50. package/coverage/server/src/routers/worksheet.ts.html +1729 -0
  51. package/coverage/server/src/trpc.ts.html +397 -0
  52. package/coverage/server/src/types/index.html +116 -0
  53. package/coverage/server/src/types/trpc.ts.html +127 -0
  54. package/coverage/server/src/utils/aiUser.ts.html +280 -0
  55. package/coverage/server/src/utils/email.ts.html +121 -0
  56. package/coverage/server/src/utils/generateInviteCode.ts.html +106 -0
  57. package/coverage/server/src/utils/index.html +206 -0
  58. package/coverage/server/src/utils/inference.ts.html +709 -0
  59. package/coverage/server/src/utils/logger.ts.html +664 -0
  60. package/coverage/server/src/utils/prismaErrorHandler.ts.html +907 -0
  61. package/coverage/server/src/utils/prismaWrapper.ts.html +355 -0
  62. package/coverage/server/vitest.config.ts.html +196 -0
  63. package/coverage/sort-arrow-sprite.png +0 -0
  64. package/coverage/sorter.js +210 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +83 -52
  67. package/dist/index.js.map +1 -1
  68. package/dist/instrument.js +15 -8
  69. package/dist/instrument.js.map +1 -1
  70. package/dist/lib/config/env.d.ts +169 -0
  71. package/dist/lib/config/env.d.ts.map +1 -0
  72. package/dist/lib/config/env.js +115 -0
  73. package/dist/lib/config/env.js.map +1 -0
  74. package/dist/lib/fileUpload.d.ts.map +1 -1
  75. package/dist/lib/fileUpload.js +5 -4
  76. package/dist/lib/fileUpload.js.map +1 -1
  77. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  78. package/dist/lib/googleCloudStorage.js +7 -8
  79. package/dist/lib/googleCloudStorage.js.map +1 -1
  80. package/dist/lib/jsonConversion.d.ts.map +1 -1
  81. package/dist/lib/jsonConversion.js +14 -16
  82. package/dist/lib/jsonConversion.js.map +1 -1
  83. package/dist/lib/notificationHandler.d.ts +2 -2
  84. package/dist/lib/prisma.d.ts +2 -2
  85. package/dist/lib/prisma.d.ts.map +1 -1
  86. package/dist/lib/prisma.js +22 -3
  87. package/dist/lib/prisma.js.map +1 -1
  88. package/dist/lib/pusher.d.ts.map +1 -1
  89. package/dist/lib/pusher.js +8 -7
  90. package/dist/lib/pusher.js.map +1 -1
  91. package/dist/middleware/auth.d.ts.map +1 -1
  92. package/dist/middleware/auth.js +7 -5
  93. package/dist/middleware/auth.js.map +1 -1
  94. package/dist/middleware/security.d.ts +5 -0
  95. package/dist/middleware/security.d.ts.map +1 -0
  96. package/dist/middleware/security.js +77 -0
  97. package/dist/middleware/security.js.map +1 -0
  98. package/dist/routers/_app.d.ts +368 -108
  99. package/dist/routers/_app.d.ts.map +1 -1
  100. package/dist/routers/_app.js +4 -2
  101. package/dist/routers/_app.js.map +1 -1
  102. package/dist/routers/agenda.d.ts.map +1 -1
  103. package/dist/routers/agenda.js +12 -9
  104. package/dist/routers/agenda.js.map +1 -1
  105. package/dist/routers/announcement.d.ts +8 -0
  106. package/dist/routers/announcement.d.ts.map +1 -1
  107. package/dist/routers/announcement.js +6 -4
  108. package/dist/routers/announcement.js.map +1 -1
  109. package/dist/routers/assignment.d.ts +17 -4
  110. package/dist/routers/assignment.d.ts.map +1 -1
  111. package/dist/routers/assignment.js +51 -19
  112. package/dist/routers/assignment.js.map +1 -1
  113. package/dist/routers/attendance.d.ts +1 -0
  114. package/dist/routers/attendance.d.ts.map +1 -1
  115. package/dist/routers/attendance.js +4 -4
  116. package/dist/routers/attendance.js.map +1 -1
  117. package/dist/routers/auth.d.ts +20 -0
  118. package/dist/routers/auth.d.ts.map +1 -1
  119. package/dist/routers/auth.js +132 -15
  120. package/dist/routers/auth.js.map +1 -1
  121. package/dist/routers/class.d.ts +10 -0
  122. package/dist/routers/class.d.ts.map +1 -1
  123. package/dist/routers/class.js +49 -5
  124. package/dist/routers/class.js.map +1 -1
  125. package/dist/routers/comment.d.ts +2 -0
  126. package/dist/routers/comment.d.ts.map +1 -1
  127. package/dist/routers/conversation.d.ts +2 -0
  128. package/dist/routers/conversation.d.ts.map +1 -1
  129. package/dist/routers/conversation.js +46 -31
  130. package/dist/routers/conversation.js.map +1 -1
  131. package/dist/routers/file.d.ts.map +1 -1
  132. package/dist/routers/file.js +30 -7
  133. package/dist/routers/file.js.map +1 -1
  134. package/dist/routers/labChat.d.ts +2 -0
  135. package/dist/routers/labChat.d.ts.map +1 -1
  136. package/dist/routers/labChat.js +5 -322
  137. package/dist/routers/labChat.js.map +1 -1
  138. package/dist/routers/marketing.d.ts +1 -1
  139. package/dist/routers/message.d.ts +1 -0
  140. package/dist/routers/message.d.ts.map +1 -1
  141. package/dist/routers/message.js +3 -2
  142. package/dist/routers/message.js.map +1 -1
  143. package/dist/routers/newtonChat.d.ts +55 -0
  144. package/dist/routers/newtonChat.d.ts.map +1 -0
  145. package/dist/routers/newtonChat.js +262 -0
  146. package/dist/routers/newtonChat.js.map +1 -0
  147. package/dist/routers/notifications.d.ts +4 -4
  148. package/dist/routers/section.d.ts +19 -4
  149. package/dist/routers/section.d.ts.map +1 -1
  150. package/dist/routers/section.js +26 -8
  151. package/dist/routers/section.js.map +1 -1
  152. package/dist/routers/user.d.ts.map +1 -1
  153. package/dist/routers/user.js +5 -4
  154. package/dist/routers/user.js.map +1 -1
  155. package/dist/routers/worksheet.d.ts +44 -41
  156. package/dist/routers/worksheet.d.ts.map +1 -1
  157. package/dist/routers/worksheet.js +25 -34
  158. package/dist/routers/worksheet.js.map +1 -1
  159. package/dist/seedDatabase.d.ts +1 -1
  160. package/dist/seedDatabase.js +275 -284
  161. package/dist/seedDatabase.js.map +1 -1
  162. package/dist/server/pipelines/aiLabChat.d.ts +21 -0
  163. package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
  164. package/dist/server/pipelines/aiLabChat.js +456 -0
  165. package/dist/server/pipelines/aiLabChat.js.map +1 -0
  166. package/dist/server/pipelines/aiNewtonChat.d.ts +30 -0
  167. package/dist/server/pipelines/aiNewtonChat.d.ts.map +1 -0
  168. package/dist/server/pipelines/aiNewtonChat.js +280 -0
  169. package/dist/server/pipelines/aiNewtonChat.js.map +1 -0
  170. package/dist/server/pipelines/gradeWorksheet.d.ts +15 -0
  171. package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
  172. package/dist/server/pipelines/gradeWorksheet.js +139 -0
  173. package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
  174. package/dist/trpc.d.ts.map +1 -1
  175. package/dist/trpc.js +2 -2
  176. package/dist/trpc.js.map +1 -1
  177. package/dist/utils/email.d.ts +9 -1
  178. package/dist/utils/email.d.ts.map +1 -1
  179. package/dist/utils/email.js +20 -5
  180. package/dist/utils/email.js.map +1 -1
  181. package/dist/utils/inference.d.ts +5 -0
  182. package/dist/utils/inference.d.ts.map +1 -1
  183. package/dist/utils/inference.js +71 -7
  184. package/dist/utils/inference.js.map +1 -1
  185. package/dist/utils/logger.d.ts.map +1 -1
  186. package/dist/utils/logger.js +3 -3
  187. package/dist/utils/logger.js.map +1 -1
  188. package/docker-compose.yml +14 -0
  189. package/package.json +13 -4
  190. package/prisma/schema.prisma +34 -5
  191. package/scripts/test-pre-push.ts +14 -0
  192. package/src/index.ts +98 -54
  193. package/src/instrument.ts +13 -6
  194. package/src/lib/config/env.ts +126 -0
  195. package/src/lib/fileUpload.ts +3 -2
  196. package/src/lib/googleCloudStorage.ts +6 -6
  197. package/src/lib/jsonConversion.ts +12 -14
  198. package/src/lib/prisma.ts +23 -2
  199. package/src/lib/pusher.ts +6 -5
  200. package/src/middleware/auth.ts +5 -3
  201. package/src/middleware/security.ts +80 -0
  202. package/src/routers/_app.ts +2 -0
  203. package/src/routers/agenda.ts +10 -7
  204. package/src/routers/announcement.ts +4 -2
  205. package/src/routers/assignment.ts +74 -41
  206. package/src/routers/attendance.ts +2 -2
  207. package/src/routers/auth.ts +143 -14
  208. package/src/routers/class.ts +52 -3
  209. package/src/routers/conversation.ts +49 -29
  210. package/src/routers/file.ts +29 -5
  211. package/src/routers/labChat.ts +3 -367
  212. package/src/routers/message.ts +1 -1
  213. package/src/routers/newtonChat.ts +299 -0
  214. package/src/routers/section.ts +26 -6
  215. package/src/routers/user.ts +3 -2
  216. package/src/routers/worksheet.ts +26 -38
  217. package/src/seedDatabase.ts +290 -283
  218. package/src/server/pipelines/aiLabChat.ts +507 -0
  219. package/src/server/pipelines/aiNewtonChat.ts +338 -0
  220. package/src/server/pipelines/gradeWorksheet.ts +151 -0
  221. package/src/trpc.ts +2 -0
  222. package/src/utils/email.ts +30 -3
  223. package/src/utils/inference.ts +85 -5
  224. package/src/utils/logger.ts +2 -1
  225. package/tests/announcement.test.ts +164 -0
  226. package/tests/assignment.test.ts +296 -0
  227. package/tests/attendance.test.ts +168 -0
  228. package/tests/auth.test.ts +33 -10
  229. package/tests/class.test.ts +34 -9
  230. package/tests/event.test.ts +228 -0
  231. package/tests/section.test.ts +216 -0
  232. package/tests/setup.ts +70 -16
  233. package/tests/user.test.ts +158 -0
  234. package/vitest.config.ts +26 -0
  235. package/API_SPECIFICATION.md +0 -1597
  236. package/BASE64_REMOVAL_SUMMARY.md +0 -164
  237. package/CHAT_API_SPEC.md +0 -579
  238. package/LAB_CHAT_API_SPEC.md +0 -518
  239. package/dist/routers/school.d.ts +0 -208
  240. package/dist/routers/school.d.ts.map +0 -1
  241. package/dist/routers/school.js +0 -483
@@ -0,0 +1,299 @@
1
+ import { z } from 'zod';
2
+ import { createTRPCRouter, protectedProcedure } from '../trpc.js';
3
+ import { prisma } from '../lib/prisma.js';
4
+ import { pusher } from '../lib/pusher.js';
5
+ import { TRPCError } from '@trpc/server';
6
+ import { logger } from '../utils/logger.js';
7
+ import { isAIUser } from '../utils/aiUser.js';
8
+ import { generateAndSendNewtonIntroduction, generateAndSendNewtonResponse } from '../server/pipelines/aiNewtonChat.js';
9
+
10
+ export const newtonChatRouter = createTRPCRouter({
11
+ getTutorConversation: protectedProcedure
12
+ .input(
13
+ z.object({
14
+ assignmentId: z.string(),
15
+ classId: z.string(),
16
+ })
17
+ )
18
+ .query(async ({ input, ctx }) => {
19
+ const userId = ctx.user!.id;
20
+ const { assignmentId, classId } = input;
21
+
22
+ // Verify user is a student in the class
23
+ const classMembership = await prisma.class.findFirst({
24
+ where: {
25
+ id: classId,
26
+ students: {
27
+ some: {
28
+ id: userId,
29
+ },
30
+ },
31
+ },
32
+ });
33
+
34
+ if (!classMembership) {
35
+ throw new TRPCError({
36
+ code: 'FORBIDDEN',
37
+ message: 'Not a student in this class',
38
+ });
39
+ }
40
+
41
+ // Find or create submission for this student and assignment
42
+ const submission = await prisma.submission.findFirst({
43
+ where: {
44
+ assignmentId,
45
+ studentId: userId,
46
+ },
47
+ });
48
+
49
+ if (!submission) {
50
+ throw new TRPCError({
51
+ code: 'NOT_FOUND',
52
+ message: 'Submission not found. Please create a submission first.',
53
+ });
54
+ }
55
+
56
+ // Find the latest NewtonChat for this submission, or create a new one
57
+ const result = await prisma.$transaction(async (tx) => {
58
+ // Get the latest NewtonChat for this submission
59
+ const existingNewtonChat = await tx.newtonChat.findFirst({
60
+ where: {
61
+ submissionId: submission.id,
62
+ },
63
+ include: {
64
+ conversation: {
65
+ include: {
66
+ members: {
67
+ where: {
68
+ userId,
69
+ },
70
+ },
71
+ },
72
+ },
73
+ },
74
+ orderBy: {
75
+ createdAt: 'desc',
76
+ },
77
+ });
78
+
79
+ // If exists and user is already a member, return it
80
+ if (existingNewtonChat && existingNewtonChat.conversation.members.length > 0) {
81
+ return existingNewtonChat;
82
+ }
83
+
84
+ // If exists but user is not a member, add them
85
+ if (existingNewtonChat) {
86
+ await tx.conversationMember.create({
87
+ data: {
88
+ userId,
89
+ conversationId: existingNewtonChat.conversationId,
90
+ role: 'MEMBER',
91
+ },
92
+ });
93
+
94
+ return existingNewtonChat;
95
+ }
96
+
97
+ // Create new NewtonChat with associated conversation
98
+ const conversation = await tx.conversation.create({
99
+ data: {
100
+ type: 'DM',
101
+ name: 'Session with Newton Tutor',
102
+ displayInChat: false, // Newton chats don't show in regular chat list
103
+ },
104
+ });
105
+
106
+ // Add student to the conversation
107
+ await tx.conversationMember.create({
108
+ data: {
109
+ userId,
110
+ conversationId: conversation.id,
111
+ role: 'MEMBER',
112
+ },
113
+ });
114
+
115
+ // Create the NewtonChat
116
+ const newtonChat = await tx.newtonChat.create({
117
+ data: {
118
+ submissionId: submission.id,
119
+ conversationId: conversation.id,
120
+ title: 'Session with Newton Tutor',
121
+ },
122
+ });
123
+
124
+ return newtonChat;
125
+ });
126
+
127
+ // Generate AI introduction message in parallel (don't await - fire and forget)
128
+ generateAndSendNewtonIntroduction(
129
+ result.id,
130
+ result.conversationId,
131
+ submission.id
132
+ ).catch(error => {
133
+ logger.error('Failed to generate AI introduction:', { error, newtonChatId: result.id });
134
+ });
135
+
136
+ return {
137
+ conversationId: result.conversationId,
138
+ newtonChatId: result.id,
139
+ };
140
+ }),
141
+
142
+ postToNewtonChat: protectedProcedure
143
+ .input(
144
+ z.object({
145
+ newtonChatId: z.string(),
146
+ content: z.string().min(1).max(4000),
147
+ mentionedUserIds: z.array(z.string()).optional(),
148
+ })
149
+ )
150
+ .mutation(async ({ input, ctx }) => {
151
+ const userId = ctx.user!.id;
152
+ const { newtonChatId, content, mentionedUserIds = [] } = input;
153
+
154
+ // Get newton chat and verify user is a member
155
+ const newtonChat = await prisma.newtonChat.findFirst({
156
+ where: {
157
+ id: newtonChatId,
158
+ conversation: {
159
+ members: {
160
+ some: {
161
+ userId,
162
+ },
163
+ },
164
+ },
165
+ },
166
+ include: {
167
+ conversation: {
168
+ select: {
169
+ id: true,
170
+ },
171
+ },
172
+ submission: {
173
+ include: {
174
+ assignment: {
175
+ select: {
176
+ id: true,
177
+ title: true,
178
+ instructions: true,
179
+ class: {
180
+ select: {
181
+ subject: true,
182
+ },
183
+ },
184
+ },
185
+ },
186
+ },
187
+ },
188
+ },
189
+ });
190
+
191
+ if (!newtonChat) {
192
+ throw new TRPCError({
193
+ code: 'FORBIDDEN',
194
+ message: 'Newton chat not found or access denied',
195
+ });
196
+ }
197
+
198
+ // Verify mentioned users are members of the conversation
199
+ if (mentionedUserIds.length > 0) {
200
+ const mentionedMemberships = await prisma.conversationMember.findMany({
201
+ where: {
202
+ conversationId: newtonChat.conversationId,
203
+ userId: { in: mentionedUserIds },
204
+ },
205
+ });
206
+
207
+ if (mentionedMemberships.length !== mentionedUserIds.length) {
208
+ throw new TRPCError({
209
+ code: 'BAD_REQUEST',
210
+ message: 'Some mentioned users are not members of this conversation',
211
+ });
212
+ }
213
+ }
214
+
215
+ // Create message and mentions
216
+ const result = await prisma.$transaction(async (tx) => {
217
+ const message = await tx.message.create({
218
+ data: {
219
+ content,
220
+ senderId: userId,
221
+ conversationId: newtonChat.conversationId,
222
+ },
223
+ include: {
224
+ sender: {
225
+ select: {
226
+ id: true,
227
+ username: true,
228
+ profile: {
229
+ select: {
230
+ displayName: true,
231
+ profilePicture: true,
232
+ },
233
+ },
234
+ },
235
+ },
236
+ },
237
+ });
238
+
239
+ // Create mentions
240
+ if (mentionedUserIds.length > 0) {
241
+ await tx.mention.createMany({
242
+ data: mentionedUserIds.map((mentionedUserId) => ({
243
+ messageId: message.id,
244
+ userId: mentionedUserId,
245
+ })),
246
+ });
247
+ }
248
+
249
+ // Update newton chat timestamp
250
+ await tx.newtonChat.update({
251
+ where: { id: newtonChatId },
252
+ data: { updatedAt: new Date() },
253
+ });
254
+
255
+ return message;
256
+ });
257
+
258
+ // Broadcast to Pusher channel (same format as regular chat)
259
+ try {
260
+ await pusher.trigger(`conversation-${newtonChat.conversationId}`, 'new-message', {
261
+ id: result.id,
262
+ content: result.content,
263
+ senderId: result.senderId,
264
+ conversationId: result.conversationId,
265
+ createdAt: result.createdAt,
266
+ sender: result.sender,
267
+ mentionedUserIds,
268
+ });
269
+ } catch (error) {
270
+ console.error('Failed to broadcast newton chat message:', error);
271
+ // Don't fail the request if Pusher fails
272
+ }
273
+
274
+ // Generate AI response in parallel (don't await - fire and forget)
275
+ if (!isAIUser(userId)) {
276
+ // Run AI response generation in background
277
+ generateAndSendNewtonResponse(
278
+ newtonChatId,
279
+ content,
280
+ newtonChat.conversationId,
281
+ newtonChat.submission
282
+ ).catch(error => {
283
+ logger.error('Failed to generate AI response:', { error });
284
+ });
285
+ }
286
+
287
+ return {
288
+ id: result.id,
289
+ content: result.content,
290
+ senderId: result.senderId,
291
+ conversationId: result.conversationId,
292
+ createdAt: result.createdAt,
293
+ sender: result.sender,
294
+ mentionedUserIds,
295
+ };
296
+ }),
297
+ });
298
+
299
+
@@ -1,10 +1,11 @@
1
1
  import { z } from "zod";
2
- import { createTRPCRouter, protectedProcedure } from "../trpc.js";
2
+ import { createTRPCRouter, protectedClassMemberProcedure, protectedProcedure, protectedTeacherProcedure } from "../trpc.js";
3
3
  import { TRPCError } from "@trpc/server";
4
4
  import { prisma } from "../lib/prisma.js";
5
5
 
6
6
  const createSectionSchema = z.object({
7
7
  classId: z.string(),
8
+ id: z.string().optional(),
8
9
  name: z.string(),
9
10
  color: z.string().optional(),
10
11
  });
@@ -22,7 +23,25 @@ const deleteSectionSchema = z.object({
22
23
  });
23
24
 
24
25
  export const sectionRouter = createTRPCRouter({
25
- create: protectedProcedure
26
+ exists: protectedClassMemberProcedure
27
+ .input(z.object({
28
+ id: z.string(),
29
+ }))
30
+ .query(async ({ ctx, input }) => {
31
+ if (!ctx.user) {
32
+ throw new TRPCError({
33
+ code: "UNAUTHORIZED",
34
+ message: "User must be authenticated",
35
+ });
36
+ }
37
+
38
+ const section = await prisma.section.findUnique({
39
+ where: { id: input.id },
40
+ });
41
+
42
+ return section ? true : false;
43
+ }),
44
+ create: protectedTeacherProcedure
26
45
  .input(createSectionSchema)
27
46
  .mutation(async ({ ctx, input }) => {
28
47
  if (!ctx.user) {
@@ -53,6 +72,7 @@ export const sectionRouter = createTRPCRouter({
53
72
 
54
73
  const section = await prisma.section.create({
55
74
  data: {
75
+ ...(input.id && { id: input.id }),
56
76
  name: input.name,
57
77
  order: 0,
58
78
  class: {
@@ -97,7 +117,7 @@ export const sectionRouter = createTRPCRouter({
97
117
  return section;
98
118
  }),
99
119
 
100
- reorder: protectedProcedure
120
+ reorder: protectedTeacherProcedure
101
121
  .input(z.object({
102
122
  classId: z.string(),
103
123
  movedId: z.string(), // Section ID
@@ -195,7 +215,7 @@ export const sectionRouter = createTRPCRouter({
195
215
  return result;
196
216
  }),
197
217
 
198
- update: protectedProcedure
218
+ update: protectedTeacherProcedure
199
219
  .input(updateSectionSchema)
200
220
  .mutation(async ({ ctx, input }) => {
201
221
  if (!ctx.user) {
@@ -237,7 +257,7 @@ export const sectionRouter = createTRPCRouter({
237
257
  return section;
238
258
  }),
239
259
 
240
- reOrder: protectedProcedure
260
+ reOrder: protectedTeacherProcedure
241
261
  .input(z.object({
242
262
  id: z.string(),
243
263
  classId: z.string(),
@@ -308,7 +328,7 @@ export const sectionRouter = createTRPCRouter({
308
328
  return { id: input.id };
309
329
  }),
310
330
 
311
- delete: protectedProcedure
331
+ delete: protectedTeacherProcedure
312
332
  .input(deleteSectionSchema)
313
333
  .mutation(async ({ ctx, input }) => {
314
334
  if (!ctx.user) {
@@ -6,6 +6,7 @@ import { createDirectUploadFiles, type DirectUploadFile } from "../lib/fileUploa
6
6
  import { getSignedUrl } from "../lib/googleCloudStorage.js";
7
7
  import { logger } from "../utils/logger.js";
8
8
  import { bucket } from "../lib/googleCloudStorage.js";
9
+ import { env } from "../lib/config/env.js";
9
10
 
10
11
  // Helper function to convert file path to backend proxy URL
11
12
  function getFileUrl(filePath: string | null): string | null {
@@ -17,7 +18,7 @@ function getFileUrl(filePath: string | null): string | null {
17
18
  }
18
19
 
19
20
  // Convert GCS path to full backend proxy URL
20
- const backendUrl = process.env.BACKEND_URL || 'http://localhost:3001';
21
+ const backendUrl = env.BACKEND_URL || 'http://localhost:3001';
21
22
  return `${backendUrl}/api/files/${encodeURIComponent(filePath)}`;
22
23
  }
23
24
 
@@ -229,7 +230,7 @@ export const userRouter = createTRPCRouter({
229
230
  const filePath = `users/${ctx.user.id}/profile/${uniqueFilename}`;
230
231
 
231
232
  // Generate backend proxy upload URL instead of direct GCS signed URL
232
- const backendUrl = process.env.BACKEND_URL || 'http://localhost:3001';
233
+ const backendUrl = env.BACKEND_URL || 'http://localhost:3001';
233
234
  const uploadUrl = `${backendUrl}/api/upload/${encodeURIComponent(filePath)}`;
234
235
 
235
236
  logger.info('Generated upload URL', {
@@ -1,8 +1,8 @@
1
1
  import { TRPCError } from "@trpc/server";
2
- import { createTRPCRouter, protectedProcedure } from "../trpc.js";
2
+ import { createTRPCRouter, protectedClassMemberProcedure, protectedProcedure } from "../trpc.js";
3
3
  import { z } from "zod";
4
4
  import { prisma } from "../lib/prisma.js";
5
- import { WorksheetQuestionType } from "@prisma/client";
5
+ import { GenerationStatus, WorksheetQuestionType } from "@prisma/client";
6
6
  import { commentSelect } from "./comment.js";
7
7
 
8
8
  type MCQOptions = {
@@ -38,6 +38,22 @@ export const worksheetRouter = createTRPCRouter({
38
38
  return worksheet;
39
39
  }),
40
40
 
41
+ exists: protectedClassMemberProcedure
42
+ .input(z.object({
43
+ id: z.string(),
44
+ }))
45
+ .query(async ({ ctx, input }) => {
46
+ if (!ctx.user) {
47
+ throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User must be authenticated' });
48
+ }
49
+
50
+ const worksheet = await prisma.worksheet.findUnique({
51
+ where: { id: input.id },
52
+ });
53
+
54
+ return worksheet ? true : false;
55
+ }),
56
+
41
57
  // List all worksheets for a class
42
58
  listWorksheets: protectedProcedure
43
59
  .input(z.object({
@@ -372,6 +388,7 @@ export const worksheetRouter = createTRPCRouter({
372
388
  data: { response,
373
389
  ...(isMarkableByAlgo && { isCorrect: response === correctAnswer }),
374
390
  ...(isMarkableByAlgo && { points: response === correctAnswer ? marksAwardedIfCorrect : 0 }),
391
+ status: GenerationStatus.NOT_STARTED,
375
392
  },
376
393
  });
377
394
  } else {
@@ -398,52 +415,23 @@ export const worksheetRouter = createTRPCRouter({
398
415
 
399
416
  return updatedWorksheetResponse;
400
417
  }),
401
- submitWorksheet: protectedProcedure
418
+ cancelAutoGrading: protectedProcedure
402
419
  .input(z.object({
403
420
  worksheetResponseId: z.string(),
421
+ questionId: z.string(),
404
422
  }))
405
423
  .mutation(async ({ ctx, input }) => {
406
- const { worksheetResponseId } = input;
424
+ const { worksheetResponseId, questionId } = input;
407
425
 
408
- const worksheetResponse = await prisma.studentWorksheetResponse.findUnique({
409
- where: { id: worksheetResponseId },
410
- include: {
411
- worksheet: {
412
- include: {
413
- questions: true,
414
- },
415
- },
416
- responses: true,
417
- },
418
- });
419
-
420
- if (!worksheetResponse) {
421
- throw new TRPCError({ code: 'NOT_FOUND', message: 'Worksheet response not found' });
422
- }
423
-
424
- if (worksheetResponse.submitted) {
425
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'Worksheet already submitted' });
426
- }
427
-
428
- // Mark worksheet as submitted
429
- const submittedWorksheet = await prisma.studentWorksheetResponse.update({
430
- where: { id: worksheetResponseId },
426
+ const updatedQuestion = await prisma.studentQuestionProgress.update({
427
+ where: { id: questionId, studentWorksheetResponseId: worksheetResponseId },
431
428
  data: {
432
- submitted: true,
433
- submittedAt: new Date(),
434
- },
435
- include: {
436
- responses: true,
429
+ status: GenerationStatus.CANCELLED,
437
430
  },
438
431
  });
439
432
 
440
- // TODO: Implement AI grading here
441
- // For now, we'll just mark all answers as pending review
442
- // You could integrate with an AI service to auto-grade certain question types
443
-
444
- return submittedWorksheet;
433
+ return updatedQuestion;
445
434
  }),
446
-
447
435
  // Grade a student's answer
448
436
  gradeAnswer: protectedProcedure
449
437
  .input(z.object({