@studious-lms/server 1.2.45 → 1.2.46

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 (231) 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 +6 -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 +294 -98
  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 +7 -4
  110. package/dist/routers/assignment.d.ts.map +1 -1
  111. package/dist/routers/assignment.js +35 -18
  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 +1 -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 +1 -0
  135. package/dist/routers/labChat.d.ts.map +1 -1
  136. package/dist/routers/labChat.js +2 -3
  137. package/dist/routers/labChat.js.map +1 -1
  138. package/dist/routers/marketing.d.ts +1 -1
  139. package/dist/routers/newtonChat.d.ts +55 -0
  140. package/dist/routers/newtonChat.d.ts.map +1 -0
  141. package/dist/routers/newtonChat.js +438 -0
  142. package/dist/routers/newtonChat.js.map +1 -0
  143. package/dist/routers/notifications.d.ts +4 -4
  144. package/dist/routers/section.d.ts +9 -4
  145. package/dist/routers/section.d.ts.map +1 -1
  146. package/dist/routers/section.js +8 -8
  147. package/dist/routers/section.js.map +1 -1
  148. package/dist/routers/user.d.ts.map +1 -1
  149. package/dist/routers/user.js +5 -4
  150. package/dist/routers/user.js.map +1 -1
  151. package/dist/routers/worksheet.d.ts +30 -36
  152. package/dist/routers/worksheet.d.ts.map +1 -1
  153. package/dist/routers/worksheet.js +11 -33
  154. package/dist/routers/worksheet.js.map +1 -1
  155. package/dist/seedDatabase.d.ts +1 -1
  156. package/dist/seedDatabase.js +275 -284
  157. package/dist/seedDatabase.js.map +1 -1
  158. package/dist/server/pipelines/aiLabChat.d.ts +10 -0
  159. package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
  160. package/dist/server/pipelines/aiLabChat.js +83 -0
  161. package/dist/server/pipelines/aiLabChat.js.map +1 -0
  162. package/dist/server/pipelines/gradeWorksheet.d.ts +2 -0
  163. package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
  164. package/dist/server/pipelines/gradeWorksheet.js +138 -0
  165. package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
  166. package/dist/trpc.d.ts.map +1 -1
  167. package/dist/trpc.js +2 -2
  168. package/dist/trpc.js.map +1 -1
  169. package/dist/utils/email.d.ts +9 -1
  170. package/dist/utils/email.d.ts.map +1 -1
  171. package/dist/utils/email.js +20 -5
  172. package/dist/utils/email.js.map +1 -1
  173. package/dist/utils/inference.d.ts +3 -0
  174. package/dist/utils/inference.d.ts.map +1 -1
  175. package/dist/utils/inference.js +41 -7
  176. package/dist/utils/inference.js.map +1 -1
  177. package/dist/utils/logger.d.ts.map +1 -1
  178. package/dist/utils/logger.js +3 -3
  179. package/dist/utils/logger.js.map +1 -1
  180. package/docker-compose.yml +14 -0
  181. package/package.json +13 -4
  182. package/prisma/schema.prisma +32 -5
  183. package/scripts/test-pre-push.ts +14 -0
  184. package/src/index.ts +98 -54
  185. package/src/instrument.ts +13 -6
  186. package/src/lib/config/env.ts +126 -0
  187. package/src/lib/fileUpload.ts +3 -2
  188. package/src/lib/googleCloudStorage.ts +6 -6
  189. package/src/lib/jsonConversion.ts +12 -14
  190. package/src/lib/prisma.ts +23 -2
  191. package/src/lib/pusher.ts +6 -5
  192. package/src/middleware/auth.ts +4 -3
  193. package/src/middleware/security.ts +80 -0
  194. package/src/routers/_app.ts +2 -0
  195. package/src/routers/agenda.ts +10 -7
  196. package/src/routers/announcement.ts +4 -2
  197. package/src/routers/assignment.ts +58 -40
  198. package/src/routers/attendance.ts +2 -2
  199. package/src/routers/auth.ts +143 -14
  200. package/src/routers/class.ts +52 -3
  201. package/src/routers/conversation.ts +49 -29
  202. package/src/routers/file.ts +29 -5
  203. package/src/routers/labChat.ts +0 -1
  204. package/src/routers/newtonChat.ts +520 -0
  205. package/src/routers/section.ts +6 -6
  206. package/src/routers/user.ts +3 -2
  207. package/src/routers/worksheet.ts +9 -37
  208. package/src/seedDatabase.ts +290 -283
  209. package/src/server/pipelines/aiLabChat.ts +92 -0
  210. package/src/server/pipelines/gradeWorksheet.ts +152 -0
  211. package/src/trpc.ts +2 -0
  212. package/src/utils/email.ts +30 -3
  213. package/src/utils/inference.ts +50 -5
  214. package/src/utils/logger.ts +2 -1
  215. package/tests/announcement.test.ts +164 -0
  216. package/tests/assignment.test.ts +296 -0
  217. package/tests/attendance.test.ts +168 -0
  218. package/tests/auth.test.ts +33 -10
  219. package/tests/class.test.ts +34 -9
  220. package/tests/event.test.ts +228 -0
  221. package/tests/section.test.ts +216 -0
  222. package/tests/setup.ts +70 -16
  223. package/tests/user.test.ts +158 -0
  224. package/vitest.config.ts +26 -0
  225. package/API_SPECIFICATION.md +0 -1597
  226. package/BASE64_REMOVAL_SUMMARY.md +0 -164
  227. package/CHAT_API_SPEC.md +0 -579
  228. package/LAB_CHAT_API_SPEC.md +0 -518
  229. package/dist/routers/school.d.ts +0 -208
  230. package/dist/routers/school.d.ts.map +0 -1
  231. package/dist/routers/school.js +0 -483
@@ -4,12 +4,13 @@ import { readFileSync } from 'fs'
4
4
  import { join } from 'path'
5
5
  import { writeFile } from 'fs'
6
6
  import { DocumentBlock, FormatTypes, Fonts } from './jsonStyles.js'
7
+ import { logger } from '../utils/logger.js'
7
8
 
8
9
  export async function createPdf(blocks: DocumentBlock[]) {
9
- console.log('createPdf: Starting PDF creation with', blocks.length, 'blocks');
10
+ logger.info(`createPdf: Starting PDF creation with ${blocks.length} blocks`);
10
11
  try {
11
12
  const pdfDoc = await PDFDocument.create()
12
- console.log('createPdf: PDFDocument created successfully');
13
+ logger.info('createPdf: PDFDocument created successfully');
13
14
 
14
15
  // Register fontkit to enable custom font embedding
15
16
  pdfDoc.registerFontkit(fontkit)
@@ -33,9 +34,9 @@ export async function createPdf(blocks: DocumentBlock[]) {
33
34
  notoSansItalic = await pdfDoc.embedFont(italicFontBytes)
34
35
  courierFont = await pdfDoc.embedFont(StandardFonts.Courier) // Keep Courier for code blocks
35
36
 
36
- console.log('createPdf: Unicode fonts loaded successfully');
37
+ logger.info('createPdf: Unicode fonts loaded successfully');
37
38
  } catch (fontError) {
38
- console.warn('createPdf: Failed to load custom fonts, falling back to standard fonts:', fontError);
39
+ logger.warn(`createPdf: Failed to load custom fonts, falling back to standard fonts: ${fontError}`);
39
40
  // Fallback to standard fonts if custom fonts fail
40
41
  notoSansRegular = await pdfDoc.embedFont(StandardFonts.Helvetica)
41
42
  notoSansBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold)
@@ -342,10 +343,10 @@ export async function createPdf(blocks: DocumentBlock[]) {
342
343
 
343
344
  let y = height - marginTop
344
345
  let lastLineHeight = -1
345
- console.log('createPdf: Starting to process', blocks.length, 'blocks');
346
+ logger.info(`createPdf: Starting to process ${blocks.length} blocks`);
346
347
  for (let i = 0; i < blocks.length; i++) {
347
348
  const block = blocks[i];
348
- console.log(`createPdf: Processing block ${i + 1}/${blocks.length}, format: ${block.format}, content type: ${typeof block.content}`);
349
+ logger.info(`createPdf: Processing block ${i + 1}/${blocks.length}, format: ${block.format}, content type: ${typeof block.content}`);
349
350
  try {
350
351
  const preset = STYLE_PRESETS[block.format] || { fontSize: defaultFontSize, lineHeight: defaultLineHeight }
351
352
 
@@ -725,24 +726,21 @@ export async function createPdf(blocks: DocumentBlock[]) {
725
726
  }
726
727
  }
727
728
  }
728
- console.log(`createPdf: Successfully processed block ${i + 1}`);
729
+ logger.info(`createPdf: Successfully processed block ${i + 1}`);
729
730
  y -= paragraphSpacing
730
731
  lastLineHeight = lineHeight
731
732
  } catch (blockError) {
732
- console.error(`createPdf: Error processing block ${i + 1}:`, blockError);
733
+ logger.error(`createPdf: Error processing block ${i + 1}: ${blockError}`);
733
734
  throw blockError;
734
735
  }
735
736
  }
736
737
 
737
- console.log('createPdf: About to save PDF document');
738
+ logger.info('createPdf: About to save PDF document');
738
739
  const pdfBytes = await pdfDoc.save()
739
- console.log('createPdf: PDF saved successfully, bytes length:', pdfBytes.length);
740
- // writeFile('output.pdf', pdfBytes, () => {
741
- // console.log('PDF created successfully') // Still only saves file, no API yet
742
- // })
740
+ logger.info(`createPdf: PDF saved successfully, bytes length: ${pdfBytes.length}`);
743
741
  return pdfBytes
744
742
  } catch (error) {
745
- console.error('createPdf: Error during PDF creation:', error);
743
+ logger.error(`createPdf: Error during PDF creation: ${error}`);
746
744
  throw error;
747
745
  }
748
746
  }
package/src/lib/prisma.ts CHANGED
@@ -1,6 +1,23 @@
1
1
  import { PrismaClient } from '@prisma/client';
2
+ import { env } from './config/env.js';
3
+
4
+ const getLogLevel = () => {
5
+ switch (env.NODE_ENV) {
6
+ case 'development':
7
+ return ['query', 'error', 'warn'];
8
+ case 'production':
9
+ return ['error'];
10
+ default:
11
+ return ['error'];
12
+ }
13
+ }
2
14
 
3
15
  const prismaClientSingleton = () => {
16
+ // return new PrismaClient({
17
+ // log: env.NODE_ENV === 'development'
18
+ // ? ['query', 'error', 'warn']
19
+ // : ['error'],
20
+ // });
4
21
  return new PrismaClient();
5
22
  };
6
23
 
@@ -11,6 +28,10 @@ declare global {
11
28
 
12
29
  export const prisma = globalThis.prisma ?? prismaClientSingleton();
13
30
 
14
- if (process.env.NODE_ENV !== 'production') {
31
+ if (env.NODE_ENV !== 'production') {
15
32
  globalThis.prisma = prisma;
16
- }
33
+ }
34
+
35
+ process.on('beforeExit', async () => {
36
+ await prisma.$disconnect();
37
+ });
package/src/lib/pusher.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import Pusher from 'pusher';
2
+ import { env } from './config/env.js';
2
3
 
3
4
  const pusher = new Pusher({
4
- appId: process.env.PUSHER_APP_ID!,
5
- key: process.env.PUSHER_KEY!,
6
- secret: process.env.PUSHER_SECRET!,
7
- cluster: process.env.PUSHER_CLUSTER!,
8
- useTLS: true,
5
+ appId: env.PUSHER_APP_ID,
6
+ key: env.PUSHER_KEY,
7
+ secret: env.PUSHER_SECRET,
8
+ cluster: env.PUSHER_CLUSTER,
9
+ useTLS: env.NODE_ENV !== 'development',
9
10
  });
10
11
 
11
12
  export { pusher };
@@ -1,6 +1,7 @@
1
1
  import { TRPCError } from '@trpc/server';
2
2
  import { prisma } from '../lib/prisma.js';
3
3
  import type { MiddlewareContext } from '../types/trpc.js';
4
+ import * as Sentry from "@sentry/node";
4
5
 
5
6
  export const createAuthMiddleware = (t: any) => {
6
7
 
@@ -50,10 +51,10 @@ export const createAuthMiddleware = (t: any) => {
50
51
  },
51
52
  });
52
53
  } catch (error) {
53
- console.log(error)
54
+ Sentry.captureException(error);
54
55
  throw new TRPCError({
55
- code: 'UNAUTHORIZED',
56
- message: 'Invalid user data',
56
+ code: 'INTERNAL_SERVER_ERROR',
57
+ message: 'Internal server error',
57
58
  });
58
59
  }
59
60
  });
@@ -0,0 +1,80 @@
1
+ import helmet from 'helmet';
2
+ import rateLimit from 'express-rate-limit';
3
+ import type { Request, Response } from 'express';
4
+
5
+ const isDevelopment = process.env.NODE_ENV === 'development';
6
+
7
+ // Custom handler for rate limit errors that returns JSON
8
+ // This format can be intercepted on the frontend with:
9
+ // error.data?.code === 'TOO_MANY_REQUESTS' || error.data?.httpStatus === 429
10
+ const rateLimitHandler = (req: Request, res: Response) => {
11
+ // Return JSON structure that can be intercepted on frontend with:
12
+ // error.data?.code === 'TOO_MANY_REQUESTS' || error.data?.httpStatus === 429
13
+ // When tRPC wraps this, the response body becomes error.data, so we put code/httpStatus at top level
14
+ res.status(429).json({
15
+ code: 'TOO_MANY_REQUESTS',
16
+ message: 'Too many requests, please try again later.',
17
+ });
18
+ };
19
+
20
+ // General API rate limiter - applies to all routes
21
+ export const generalLimiter = rateLimit({
22
+ windowMs: 10 * 60 * 1000, // 10 minutes
23
+ max: 100, // Limit each IP to 100 requests per windowMs
24
+ message: 'Too many requests from this IP, please try again later.',
25
+ standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
26
+ legacyHeaders: false, // Disable the `X-RateLimit-*` headers
27
+ handler: rateLimitHandler,
28
+ skip: (req) => {
29
+ // Skip rate limiting for health checks
30
+ return req.path === '/health';
31
+ },
32
+ });
33
+
34
+ // Stricter rate limiter for authentication endpoints
35
+ export const authLimiter = rateLimit({
36
+ windowMs: 5 * 60 * 1000, // 5 minutes
37
+ max: 5, // Limit each IP to 5 login attempts per windowMs
38
+ message: 'Too many authentication attempts, please try again later.',
39
+ standardHeaders: true,
40
+ legacyHeaders: false,
41
+ skipSuccessfulRequests: true, // Don't count successful requests
42
+ handler: rateLimitHandler,
43
+ });
44
+
45
+ // File upload rate limiter
46
+ export const uploadLimiter = rateLimit({
47
+ windowMs: 30 * 60 * 1000, // 30 minutes
48
+ max: 50, // Limit each IP to 50 uploads per hour
49
+ message: 'Too many file uploads, please try again later.',
50
+ standardHeaders: true,
51
+ legacyHeaders: false,
52
+ handler: rateLimitHandler,
53
+ });
54
+
55
+ // Helmet configuration
56
+ export const helmetConfig = helmet({
57
+ contentSecurityPolicy: {
58
+ directives: {
59
+ defaultSrc: ["'self'"],
60
+ styleSrc: ["'self'", "'unsafe-inline'"], // Allow inline styles for tRPC panel
61
+ // Allow inline scripts only in development (for tRPC panel)
62
+ // In production, keep strict CSP without unsafe-inline
63
+ scriptSrc: isDevelopment
64
+ ? ["'self'", "'unsafe-inline'"]
65
+ : ["'self'"],
66
+ imgSrc: ["'self'", "data:", "https:"], // Allow images from any HTTPS source
67
+ connectSrc: ["'self'", "https://*.sentry.io"], // Allow Sentry connections
68
+ fontSrc: ["'self'", "data:"],
69
+ objectSrc: ["'none'"],
70
+ mediaSrc: ["'self'"],
71
+ frameSrc: ["'none'"],
72
+ },
73
+ },
74
+ crossOriginEmbedderPolicy: false, // Disable if you need to embed resources
75
+ hsts: {
76
+ maxAge: 31536000, // 1 year
77
+ includeSubDomains: true,
78
+ preload: true,
79
+ },
80
+ });
@@ -16,6 +16,7 @@ import { notificationRouter } from "./notifications.js";
16
16
  import { conversationRouter } from "./conversation.js";
17
17
  import { messageRouter } from "./message.js";
18
18
  import { labChatRouter } from "./labChat.js";
19
+ import { newtonChatRouter } from "./newtonChat.js";
19
20
  import { marketingRouter } from "./marketing.js";
20
21
  import { worksheetRouter } from "./worksheet.js";
21
22
  import { commentRouter } from "./comment.js";
@@ -36,6 +37,7 @@ export const appRouter = createTRPCRouter({
36
37
  conversation: conversationRouter,
37
38
  message: messageRouter,
38
39
  labChat: labChatRouter,
40
+ newtonChat: newtonChatRouter,
39
41
  marketing: marketingRouter,
40
42
  worksheet: worksheetRouter,
41
43
  comment: commentRouter,
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { createTRPCRouter, protectedProcedure } from "../trpc.js";
3
3
  import { prisma } from "../lib/prisma.js";
4
4
  import { TRPCError } from "@trpc/server";
5
- import { addDays, startOfDay, endOfDay } from "date-fns";
5
+ import { addDays, addMonths, subMonths, startOfDay, endOfDay } from "date-fns";
6
6
 
7
7
  export const agendaRouter = createTRPCRouter({
8
8
  get: protectedProcedure
@@ -17,8 +17,11 @@ export const agendaRouter = createTRPCRouter({
17
17
  });
18
18
  }
19
19
 
20
- const weekStart = startOfDay(new Date(input.weekStart));
21
- const weekEnd = endOfDay(addDays(weekStart, 6));
20
+ // Expand query range to 6 months (3 months before and after the reference date)
21
+ // to allow calendar navigation and ensure newly created events are visible
22
+ const referenceDate = new Date(input.weekStart);
23
+ const rangeStart = startOfDay(subMonths(referenceDate, 3));
24
+ const rangeEnd = endOfDay(addMonths(referenceDate, 3));
22
25
 
23
26
  const [personalEvents, classEvents] = await Promise.all([
24
27
  // Get personal events
@@ -26,8 +29,8 @@ export const agendaRouter = createTRPCRouter({
26
29
  where: {
27
30
  userId: ctx.user.id,
28
31
  startTime: {
29
- gte: weekStart,
30
- lte: weekEnd,
32
+ gte: rangeStart,
33
+ lte: rangeEnd,
31
34
  },
32
35
  class: {
33
36
  is: null,
@@ -59,8 +62,8 @@ export const agendaRouter = createTRPCRouter({
59
62
  ],
60
63
  },
61
64
  startTime: {
62
- gte: weekStart,
63
- lte: weekEnd,
65
+ gte: rangeStart,
66
+ lte: rangeEnd,
64
67
  },
65
68
  },
66
69
  include: {
@@ -206,9 +206,10 @@ export const announcementRouter = createTRPCRouter({
206
206
  };
207
207
  }),
208
208
 
209
- update: protectedProcedure
209
+ update: protectedTeacherProcedure
210
210
  .input(z.object({
211
211
  id: z.string(),
212
+ classId: z.string(),
212
213
  data: z.object({
213
214
  remarks: z.string().min(1, "Remarks cannot be empty").optional(),
214
215
  files: z.array(directFileSchema).optional(),
@@ -339,9 +340,10 @@ export const announcementRouter = createTRPCRouter({
339
340
  };
340
341
  }),
341
342
 
342
- delete: protectedProcedure
343
+ delete: protectedTeacherProcedure
343
344
  .input(z.object({
344
345
  id: z.string(),
346
+ classId: z.string(),
345
347
  }))
346
348
  .mutation(async ({ ctx, input }) => {
347
349
  if (!ctx.user) {
@@ -6,6 +6,7 @@ import { createDirectUploadFiles, type DirectUploadFile, confirmDirectUpload, up
6
6
  import { deleteFile } from "../lib/googleCloudStorage.js";
7
7
  import { sendNotifications } from "../lib/notificationHandler.js";
8
8
  import { logger } from "../utils/logger.js";
9
+ import { gradeWorksheetPipeline } from "../server/pipelines/gradeWorksheet.js";
9
10
 
10
11
  // DEPRECATED: This schema is no longer used - files are uploaded directly to GCS
11
12
  // Use directFileSchema instead
@@ -173,11 +174,11 @@ async function getUnifiedList(tx: any, classId: string) {
173
174
  // Updated to batch updates to prevent timeouts with large lists
174
175
  async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Array<{ id: string; type: 'section' | 'assignment' }>) {
175
176
  const BATCH_SIZE = 10; // Process 10 items at a time to avoid overwhelming the transaction
176
-
177
+
177
178
  // Group items by type for more efficient updates
178
179
  const sections: Array<{ id: string; order: number }> = [];
179
180
  const assignments: Array<{ id: string; order: number }> = [];
180
-
181
+
181
182
  orderedItems.forEach((item, index) => {
182
183
  const orderData = { id: item.id, order: index + 1 };
183
184
  if (item.type === 'section') {
@@ -186,7 +187,7 @@ async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Arra
186
187
  assignments.push(orderData);
187
188
  }
188
189
  });
189
-
190
+
190
191
  // Process updates in batches
191
192
  const processBatch = async (items: Array<{ id: string; order: number }>, type: 'section' | 'assignment') => {
192
193
  for (let i = 0; i < items.length; i += BATCH_SIZE) {
@@ -202,7 +203,7 @@ async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Arra
202
203
  );
203
204
  }
204
205
  };
205
-
206
+
206
207
  // Process sections and assignments sequentially to avoid transaction overload
207
208
  await processBatch(sections, 'section');
208
209
  await processBatch(assignments, 'assignment');
@@ -318,7 +319,7 @@ export const assignmentRouter = createTRPCRouter({
318
319
  return updated;
319
320
  }),
320
321
 
321
- move: protectedTeacherProcedure
322
+ move: protectedTeacherProcedure
322
323
  .input(z.object({
323
324
  id: z.string(),
324
325
  classId: z.string(),
@@ -354,7 +355,7 @@ export const assignmentRouter = createTRPCRouter({
354
355
  return updated;
355
356
  }),
356
357
 
357
- create: protectedProcedure
358
+ create: protectedTeacherProcedure
358
359
  .input(createAssignmentSchema)
359
360
  .mutation(async ({ ctx, input }) => {
360
361
  const { classId, title, instructions, dueDate, files, existingFileIds, aiPolicyLevel, acceptFiles, acceptExtendedResponse, acceptWorksheet, worksheetIds, gradeWithAI, studentIds, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId, inProgress } = input;
@@ -401,16 +402,14 @@ export const assignmentRouter = createTRPCRouter({
401
402
  }, 0);
402
403
  }
403
404
 
404
- console.log(studentIds);
405
-
406
405
  // Prepare submission data outside transaction
407
- const submissionData = studentIds && studentIds.length > 0
406
+ const submissionData = studentIds && studentIds.length > 0
408
407
  ? studentIds.map((studentId) => ({
409
- student: { connect: { id: studentId } }
410
- }))
408
+ student: { connect: { id: studentId } }
409
+ }))
411
410
  : classData.students.map((student) => ({
412
- student: { connect: { id: student.id } }
413
- }));
411
+ student: { connect: { id: student.id } }
412
+ }));
414
413
 
415
414
  const teacherId = ctx.user.id;
416
415
 
@@ -516,7 +515,7 @@ export const assignmentRouter = createTRPCRouter({
516
515
  order: { increment: 1 }
517
516
  }
518
517
  });
519
-
518
+
520
519
  await tx.section.updateMany({
521
520
  where: {
522
521
  classId: classId,
@@ -581,12 +580,12 @@ export const assignmentRouter = createTRPCRouter({
581
580
 
582
581
  // Execute file operations in parallel
583
582
  await Promise.all(fileOperations);
584
-
583
+
585
584
  // Send notifications asynchronously (non-blocking)
586
585
  sendNotifications(classData.students.map(student => student.id), {
587
586
  title: `🔔 New assignment for ${classData.name}`,
588
587
  content:
589
- `The assignment "${title}" has been created in ${classData.name}.\n
588
+ `The assignment "${title}" has been created in ${classData.name}.\n
590
589
  Due date: ${new Date(dueDate).toLocaleDateString()}.
591
590
  [Link to assignment](/class/${classId}/assignments/${assignment.id})`
592
591
  }).catch(error => {
@@ -595,7 +594,7 @@ export const assignmentRouter = createTRPCRouter({
595
594
 
596
595
  return assignment;
597
596
  }),
598
- update: protectedProcedure
597
+ update: protectedTeacherProcedure
599
598
  .input(updateAssignmentSchema)
600
599
  .mutation(async ({ ctx, input }) => {
601
600
  const { id, title, instructions, dueDate, files, existingFileIds, worksheetIds, aiPolicyLevel, maxGrade, graded, weight, sectionId, type, inProgress, acceptFiles, acceptExtendedResponse, acceptWorksheet, gradeWithAI, studentIds } = input;
@@ -640,10 +639,10 @@ export const assignmentRouter = createTRPCRouter({
640
639
  },
641
640
  }),
642
641
  prisma.class.findFirst({
643
- where: {
644
- assignments: {
645
- some: { id }
646
- }
642
+ where: {
643
+ assignments: {
644
+ some: { id }
645
+ }
647
646
  },
648
647
  include: {
649
648
  students: {
@@ -661,13 +660,13 @@ export const assignmentRouter = createTRPCRouter({
661
660
  }
662
661
 
663
662
  // Prepare submission data outside transaction if needed
664
- const submissionData = studentIds && studentIds.length > 0
663
+ const submissionData = studentIds && studentIds.length > 0
665
664
  ? studentIds.map((studentId) => ({
666
- student: { connect: { id: studentId } }
667
- }))
665
+ student: { connect: { id: studentId } }
666
+ }))
668
667
  : classData?.students.map((student) => ({
669
- student: { connect: { id: student.id } }
670
- }));
668
+ student: { connect: { id: student.id } }
669
+ }));
671
670
 
672
671
  // Handle file deletion operations outside transaction
673
672
  const fileDeletionPromises: Promise<void>[] = [];
@@ -929,7 +928,7 @@ export const assignmentRouter = createTRPCRouter({
929
928
  };
930
929
  }),
931
930
 
932
- get: protectedProcedure
931
+ get: protectedClassMemberProcedure
933
932
  .input(getAssignmentSchema)
934
933
  .query(async ({ ctx, input }) => {
935
934
  const { id, classId } = input;
@@ -1140,19 +1139,12 @@ export const assignmentRouter = createTRPCRouter({
1140
1139
  };
1141
1140
  }),
1142
1141
 
1143
- getSubmissionById: protectedTeacherProcedure
1142
+ getSubmissionById: protectedClassMemberProcedure
1144
1143
  .input(z.object({
1145
- submissionId: z.string(),
1146
1144
  classId: z.string(),
1145
+ submissionId: z.string(),
1147
1146
  }))
1148
1147
  .query(async ({ ctx, input }) => {
1149
- if (!ctx.user) {
1150
- throw new TRPCError({
1151
- code: "UNAUTHORIZED",
1152
- message: "User must be authenticated",
1153
- });
1154
- }
1155
-
1156
1148
  const { submissionId, classId } = input;
1157
1149
 
1158
1150
  const submission = await prisma.submission.findFirst({
@@ -1161,11 +1153,22 @@ export const assignmentRouter = createTRPCRouter({
1161
1153
  assignment: {
1162
1154
  classId,
1163
1155
  class: {
1164
- teachers: {
1165
- some: {
1166
- id: ctx.user.id
1156
+ OR: [
1157
+ {
1158
+ teachers: {
1159
+ some: {
1160
+ id: ctx.user?.id
1161
+ }
1162
+ }
1163
+ },
1164
+ {
1165
+ students: {
1166
+ some: {
1167
+ id: ctx.user?.id
1168
+ }
1169
+ }
1167
1170
  }
1168
- }
1171
+ ],
1169
1172
  }
1170
1173
  },
1171
1174
  },
@@ -1287,6 +1290,21 @@ export const assignmentRouter = createTRPCRouter({
1287
1290
 
1288
1291
  if (submit !== undefined) {
1289
1292
  // Toggle submission status
1293
+
1294
+ if (submission.assignment.acceptWorksheet && submission.assignment.gradeWithAI) {
1295
+
1296
+ // Grade the submission with AI
1297
+ const worksheetResponses = await prisma.studentWorksheetResponse.findMany({
1298
+ where: {
1299
+ submissionId: submission.id,
1300
+ },
1301
+ });
1302
+
1303
+ for (const worksheetResponse of worksheetResponses) {
1304
+ // Run it in the background, non-blocking
1305
+ gradeWorksheetPipeline(worksheetResponse.id);
1306
+ }
1307
+ }
1290
1308
  return await prisma.submission.update({
1291
1309
  where: { id: submission.id },
1292
1310
  data: {
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { createTRPCRouter, protectedProcedure } from "../trpc.js";
2
+ import { createTRPCRouter, protectedClassMemberProcedure, protectedProcedure } from "../trpc.js";
3
3
  import { TRPCError } from "@trpc/server";
4
4
  import { prisma } from "../lib/prisma.js";
5
5
 
@@ -11,7 +11,7 @@ const attendanceSchema = z.object({
11
11
  });
12
12
 
13
13
  export const attendanceRouter = createTRPCRouter({
14
- get: protectedProcedure
14
+ get: protectedClassMemberProcedure
15
15
  .input(z.object({
16
16
  classId: z.string(),
17
17
  eventId: z.string().optional(),