@studious-lms/server 1.2.44 → 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 (234) 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 +304 -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 +7 -0
  126. package/dist/routers/comment.d.ts.map +1 -1
  127. package/dist/routers/comment.js +9 -2
  128. package/dist/routers/comment.js.map +1 -1
  129. package/dist/routers/conversation.d.ts +1 -0
  130. package/dist/routers/conversation.d.ts.map +1 -1
  131. package/dist/routers/conversation.js +46 -31
  132. package/dist/routers/conversation.js.map +1 -1
  133. package/dist/routers/file.d.ts.map +1 -1
  134. package/dist/routers/file.js +30 -7
  135. package/dist/routers/file.js.map +1 -1
  136. package/dist/routers/labChat.d.ts +1 -0
  137. package/dist/routers/labChat.d.ts.map +1 -1
  138. package/dist/routers/labChat.js +2 -3
  139. package/dist/routers/labChat.js.map +1 -1
  140. package/dist/routers/marketing.d.ts +1 -1
  141. package/dist/routers/newtonChat.d.ts +55 -0
  142. package/dist/routers/newtonChat.d.ts.map +1 -0
  143. package/dist/routers/newtonChat.js +438 -0
  144. package/dist/routers/newtonChat.js.map +1 -0
  145. package/dist/routers/notifications.d.ts +4 -4
  146. package/dist/routers/section.d.ts +9 -4
  147. package/dist/routers/section.d.ts.map +1 -1
  148. package/dist/routers/section.js +8 -8
  149. package/dist/routers/section.js.map +1 -1
  150. package/dist/routers/user.d.ts.map +1 -1
  151. package/dist/routers/user.js +5 -4
  152. package/dist/routers/user.js.map +1 -1
  153. package/dist/routers/worksheet.d.ts +30 -36
  154. package/dist/routers/worksheet.d.ts.map +1 -1
  155. package/dist/routers/worksheet.js +11 -33
  156. package/dist/routers/worksheet.js.map +1 -1
  157. package/dist/seedDatabase.d.ts +1 -1
  158. package/dist/seedDatabase.js +275 -284
  159. package/dist/seedDatabase.js.map +1 -1
  160. package/dist/server/pipelines/aiLabChat.d.ts +10 -0
  161. package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
  162. package/dist/server/pipelines/aiLabChat.js +83 -0
  163. package/dist/server/pipelines/aiLabChat.js.map +1 -0
  164. package/dist/server/pipelines/gradeWorksheet.d.ts +2 -0
  165. package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
  166. package/dist/server/pipelines/gradeWorksheet.js +138 -0
  167. package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
  168. package/dist/trpc.d.ts.map +1 -1
  169. package/dist/trpc.js +2 -2
  170. package/dist/trpc.js.map +1 -1
  171. package/dist/utils/email.d.ts +9 -1
  172. package/dist/utils/email.d.ts.map +1 -1
  173. package/dist/utils/email.js +20 -5
  174. package/dist/utils/email.js.map +1 -1
  175. package/dist/utils/inference.d.ts +3 -0
  176. package/dist/utils/inference.d.ts.map +1 -1
  177. package/dist/utils/inference.js +41 -7
  178. package/dist/utils/inference.js.map +1 -1
  179. package/dist/utils/logger.d.ts.map +1 -1
  180. package/dist/utils/logger.js +3 -3
  181. package/dist/utils/logger.js.map +1 -1
  182. package/docker-compose.yml +14 -0
  183. package/package.json +13 -4
  184. package/prisma/schema.prisma +32 -5
  185. package/scripts/test-pre-push.ts +14 -0
  186. package/src/index.ts +98 -54
  187. package/src/instrument.ts +13 -6
  188. package/src/lib/config/env.ts +126 -0
  189. package/src/lib/fileUpload.ts +3 -2
  190. package/src/lib/googleCloudStorage.ts +6 -6
  191. package/src/lib/jsonConversion.ts +12 -14
  192. package/src/lib/prisma.ts +23 -2
  193. package/src/lib/pusher.ts +6 -5
  194. package/src/middleware/auth.ts +4 -3
  195. package/src/middleware/security.ts +80 -0
  196. package/src/routers/_app.ts +2 -0
  197. package/src/routers/agenda.ts +10 -7
  198. package/src/routers/announcement.ts +4 -2
  199. package/src/routers/assignment.ts +58 -40
  200. package/src/routers/attendance.ts +2 -2
  201. package/src/routers/auth.ts +143 -14
  202. package/src/routers/class.ts +52 -3
  203. package/src/routers/comment.ts +7 -0
  204. package/src/routers/conversation.ts +49 -29
  205. package/src/routers/file.ts +29 -5
  206. package/src/routers/labChat.ts +0 -1
  207. package/src/routers/newtonChat.ts +520 -0
  208. package/src/routers/section.ts +6 -6
  209. package/src/routers/user.ts +3 -2
  210. package/src/routers/worksheet.ts +9 -37
  211. package/src/seedDatabase.ts +290 -283
  212. package/src/server/pipelines/aiLabChat.ts +92 -0
  213. package/src/server/pipelines/gradeWorksheet.ts +152 -0
  214. package/src/trpc.ts +2 -0
  215. package/src/utils/email.ts +30 -3
  216. package/src/utils/inference.ts +50 -5
  217. package/src/utils/logger.ts +2 -1
  218. package/tests/announcement.test.ts +164 -0
  219. package/tests/assignment.test.ts +296 -0
  220. package/tests/attendance.test.ts +168 -0
  221. package/tests/auth.test.ts +33 -10
  222. package/tests/class.test.ts +34 -9
  223. package/tests/event.test.ts +228 -0
  224. package/tests/section.test.ts +216 -0
  225. package/tests/setup.ts +70 -16
  226. package/tests/user.test.ts +158 -0
  227. package/vitest.config.ts +26 -0
  228. package/API_SPECIFICATION.md +0 -1597
  229. package/BASE64_REMOVAL_SUMMARY.md +0 -164
  230. package/CHAT_API_SPEC.md +0 -579
  231. package/LAB_CHAT_API_SPEC.md +0 -518
  232. package/dist/routers/school.d.ts +0 -208
  233. package/dist/routers/school.d.ts.map +0 -1
  234. package/dist/routers/school.js +0 -483
@@ -1,483 +0,0 @@
1
- import { z } from 'zod';
2
- import { createTRPCRouter, protectedProcedure } from '../trpc';
3
- import { prisma } from '../lib/prisma';
4
- import { TRPCError } from '@trpc/server';
5
- import { UserRole } from '@prisma/client';
6
- import { transport } from '../utils/email';
7
- import { generateInviteCode } from '../utils/generateInviteCode';
8
- import { hash } from 'bcryptjs';
9
- export const schoolRouter = createTRPCRouter({
10
- // Create a new school
11
- createSchool: protectedProcedure
12
- .input(z.object({
13
- name: z.string().min(1),
14
- subdomain: z.string().regex(/^[a-z0-9-]+$/, 'Subdomain can only contain lowercase letters, numbers, and hyphens')
15
- }))
16
- .mutation(async ({ input, ctx }) => {
17
- if (!ctx.user) {
18
- throw new TRPCError({
19
- code: 'UNAUTHORIZED',
20
- message: 'User must be authenticated'
21
- });
22
- }
23
- // Check if subdomain is already taken
24
- const existingSchool = await prisma.school.findUnique({
25
- where: { subdomain: input.subdomain }
26
- });
27
- if (existingSchool) {
28
- throw new TRPCError({
29
- code: 'CONFLICT',
30
- message: 'This subdomain is already taken'
31
- });
32
- }
33
- // Create a placeholder file for the logo
34
- const placeholderLogo = await prisma.file.create({
35
- data: {
36
- name: 'placeholder-logo',
37
- path: '/placeholder-logo.png',
38
- type: 'image/png',
39
- userId: ctx.user.id
40
- }
41
- });
42
- // Create the school
43
- const school = await prisma.school.create({
44
- data: {
45
- name: input.name,
46
- subdomain: input.subdomain,
47
- logoId: placeholderLogo.id,
48
- users: {
49
- connect: { id: ctx.user.id }
50
- }
51
- }
52
- });
53
- // Update the user to be an admin of this school
54
- await prisma.user.update({
55
- where: { id: ctx.user.id },
56
- data: {
57
- role: UserRole.ADMIN,
58
- schoolId: school.id
59
- }
60
- });
61
- return school;
62
- }),
63
- // Check if user is admin of the school
64
- checkAdmin: protectedProcedure
65
- .input(z.object({ schoolId: z.string() }))
66
- .query(async ({ input, ctx }) => {
67
- if (!ctx.user) {
68
- throw new TRPCError({
69
- code: 'UNAUTHORIZED',
70
- message: 'User must be authenticated'
71
- });
72
- }
73
- const user = await prisma.user.findFirst({
74
- where: {
75
- id: ctx.user.id,
76
- schoolId: input.schoolId,
77
- role: UserRole.ADMIN
78
- }
79
- });
80
- if (!user) {
81
- throw new TRPCError({
82
- code: 'FORBIDDEN',
83
- message: 'You must be a school admin to access this'
84
- });
85
- }
86
- return true;
87
- }),
88
- // Get all users in a school
89
- getUsers: protectedProcedure
90
- .input(z.object({
91
- schoolId: z.string(),
92
- role: z.enum(['STUDENT', 'TEACHER', 'ADMIN', 'NONE']).optional()
93
- }))
94
- .query(async ({ input, ctx }) => {
95
- if (!ctx.user) {
96
- throw new TRPCError({
97
- code: 'UNAUTHORIZED',
98
- message: 'User must be authenticated'
99
- });
100
- }
101
- // Check admin permission
102
- const isAdmin = await prisma.user.findFirst({
103
- where: {
104
- id: ctx.user.id,
105
- schoolId: input.schoolId,
106
- role: UserRole.ADMIN
107
- }
108
- });
109
- if (!isAdmin) {
110
- throw new TRPCError({ code: 'FORBIDDEN' });
111
- }
112
- return prisma.user.findMany({
113
- where: {
114
- schoolId: input.schoolId,
115
- ...(input.role && { role: input.role })
116
- },
117
- select: {
118
- id: true,
119
- username: true,
120
- email: true,
121
- role: true,
122
- verified: true,
123
- profile: true
124
- },
125
- orderBy: [
126
- { role: 'asc' },
127
- { username: 'asc' }
128
- ]
129
- });
130
- }),
131
- // Create a new user
132
- createUser: protectedProcedure
133
- .input(z.object({
134
- schoolId: z.string(),
135
- email: z.string().email(),
136
- username: z.string().min(3),
137
- role: z.enum(['STUDENT', 'TEACHER', 'ADMIN', 'NONE']),
138
- sendInvite: z.boolean().default(true)
139
- }))
140
- .mutation(async ({ input, ctx }) => {
141
- // Check admin permission
142
- const isAdmin = await prisma.user.findFirst({
143
- where: {
144
- id: ctx.user.id,
145
- schoolId: input.schoolId,
146
- role: UserRole.ADMIN
147
- }
148
- });
149
- if (!isAdmin) {
150
- throw new TRPCError({ code: 'FORBIDDEN' });
151
- }
152
- // Check if user already exists
153
- const existingUser = await prisma.user.findFirst({
154
- where: {
155
- OR: [
156
- { email: input.email },
157
- { username: input.username }
158
- ]
159
- }
160
- });
161
- if (existingUser) {
162
- throw new TRPCError({
163
- code: 'CONFLICT',
164
- message: 'User with this email or username already exists'
165
- });
166
- }
167
- // Generate temporary password
168
- const tempPassword = generateInviteCode();
169
- const hashedPassword = await hash(tempPassword, 10);
170
- const user = await prisma.user.create({
171
- data: {
172
- email: input.email,
173
- username: input.username,
174
- password: hashedPassword,
175
- role: input.role,
176
- schoolId: input.schoolId,
177
- verified: false
178
- }
179
- });
180
- // Send invite email
181
- if (input.sendInvite) {
182
- await transport.sendMail({
183
- from: process.env.EMAIL_FROM || 'noreply@studious.app',
184
- to: input.email,
185
- subject: 'Welcome to Studious',
186
- text: `You have been invited to join Studious. Your temporary password is: ${tempPassword}\n\nPlease change your password after logging in.`
187
- });
188
- }
189
- return user;
190
- }),
191
- // Bulk create users from JSON
192
- bulkCreateUsers: protectedProcedure
193
- .input(z.object({
194
- schoolId: z.string(),
195
- users: z.array(z.object({
196
- email: z.string().email(),
197
- username: z.string().min(3),
198
- role: z.enum(['STUDENT', 'TEACHER', 'ADMIN', 'NONE'])
199
- }))
200
- }))
201
- .mutation(async ({ input, ctx }) => {
202
- if (!ctx.user) {
203
- throw new TRPCError({
204
- code: 'UNAUTHORIZED',
205
- message: 'User must be authenticated'
206
- });
207
- }
208
- // Check admin permission
209
- const isAdmin = await prisma.user.findFirst({
210
- where: {
211
- id: ctx.user.id,
212
- schoolId: input.schoolId,
213
- role: UserRole.ADMIN
214
- }
215
- });
216
- if (!isAdmin) {
217
- throw new TRPCError({ code: 'FORBIDDEN' });
218
- }
219
- const results = [];
220
- const errors = [];
221
- for (const userData of input.users) {
222
- try {
223
- const tempPassword = generateInviteCode();
224
- const hashedPassword = await hash(tempPassword, 10);
225
- const user = await prisma.user.create({
226
- data: {
227
- email: userData.email,
228
- username: userData.username,
229
- password: hashedPassword,
230
- role: userData.role,
231
- schoolId: input.schoolId,
232
- verified: false
233
- }
234
- });
235
- // Send invite email
236
- await transport.sendMail({
237
- from: process.env.EMAIL_FROM || 'noreply@studious.app',
238
- to: userData.email,
239
- subject: 'Welcome to Studious',
240
- text: `You have been invited to join Studious. Your temporary password is: ${tempPassword}\n\nPlease change your password after logging in.`
241
- });
242
- results.push({ success: true, user });
243
- }
244
- catch (error) {
245
- errors.push({
246
- email: userData.email,
247
- error: error instanceof Error ? error.message : 'Unknown error'
248
- });
249
- }
250
- }
251
- return { results, errors };
252
- }),
253
- // Update user
254
- updateUser: protectedProcedure
255
- .input(z.object({
256
- userId: z.string(),
257
- schoolId: z.string(),
258
- role: z.enum(['STUDENT', 'TEACHER', 'ADMIN', 'NONE']).optional(),
259
- verified: z.boolean().optional()
260
- }))
261
- .mutation(async ({ input, ctx }) => {
262
- if (!ctx.user) {
263
- throw new TRPCError({
264
- code: 'UNAUTHORIZED',
265
- message: 'User must be authenticated'
266
- });
267
- }
268
- // Check admin permission
269
- const isAdmin = await prisma.user.findFirst({
270
- where: {
271
- id: ctx.user.id,
272
- schoolId: input.schoolId,
273
- role: UserRole.ADMIN
274
- }
275
- });
276
- if (!isAdmin) {
277
- throw new TRPCError({ code: 'FORBIDDEN' });
278
- }
279
- return prisma.user.update({
280
- where: { id: input.userId },
281
- data: {
282
- ...(input.role && { role: input.role }),
283
- ...(input.verified !== undefined && { verified: input.verified })
284
- }
285
- });
286
- }),
287
- // Delete user
288
- deleteUser: protectedProcedure
289
- .input(z.object({
290
- userId: z.string(),
291
- schoolId: z.string()
292
- }))
293
- .mutation(async ({ input, ctx }) => {
294
- if (!ctx.user) {
295
- throw new TRPCError({
296
- code: 'UNAUTHORIZED',
297
- message: 'User must be authenticated'
298
- });
299
- }
300
- // Check admin permission
301
- const isAdmin = await prisma.user.findFirst({
302
- where: {
303
- id: ctx.user.id,
304
- schoolId: input.schoolId,
305
- role: UserRole.ADMIN
306
- }
307
- });
308
- if (!isAdmin) {
309
- throw new TRPCError({ code: 'FORBIDDEN' });
310
- }
311
- // Don't allow deleting yourself
312
- if (input.userId === ctx.user.id) {
313
- throw new TRPCError({
314
- code: 'BAD_REQUEST',
315
- message: 'You cannot delete yourself'
316
- });
317
- }
318
- return prisma.user.delete({
319
- where: { id: input.userId }
320
- });
321
- }),
322
- // Get school info
323
- getSchool: protectedProcedure
324
- .input(z.object({ schoolId: z.string() }))
325
- .query(async ({ input }) => {
326
- return prisma.school.findUnique({
327
- where: { id: input.schoolId },
328
- include: {
329
- logo: true,
330
- _count: {
331
- select: {
332
- users: true,
333
- classes: true
334
- }
335
- }
336
- }
337
- });
338
- }),
339
- // Update school settings
340
- updateSchool: protectedProcedure
341
- .input(z.object({
342
- schoolId: z.string(),
343
- name: z.string().optional(),
344
- subdomain: z.string().optional(),
345
- logoId: z.string().optional()
346
- }))
347
- .mutation(async ({ input, ctx }) => {
348
- if (!ctx.user) {
349
- throw new TRPCError({
350
- code: 'UNAUTHORIZED',
351
- message: 'User must be authenticated'
352
- });
353
- }
354
- // Check admin permission
355
- const isAdmin = await prisma.user.findFirst({
356
- where: {
357
- id: ctx.user.id,
358
- schoolId: input.schoolId,
359
- role: UserRole.ADMIN
360
- }
361
- });
362
- if (!isAdmin) {
363
- throw new TRPCError({ code: 'FORBIDDEN' });
364
- }
365
- const updateData = {};
366
- if (input.name)
367
- updateData.name = input.name;
368
- if (input.subdomain)
369
- updateData.subdomain = input.subdomain;
370
- if (input.logoId)
371
- updateData.logoId = input.logoId;
372
- return prisma.school.update({
373
- where: { id: input.schoolId },
374
- data: updateData
375
- });
376
- }),
377
- // Get all classes in school
378
- getClasses: protectedProcedure
379
- .input(z.object({ schoolId: z.string() }))
380
- .query(async ({ input, ctx }) => {
381
- if (!ctx.user) {
382
- throw new TRPCError({
383
- code: 'UNAUTHORIZED',
384
- message: 'User must be authenticated'
385
- });
386
- }
387
- // Check if user belongs to school
388
- const user = await prisma.user.findFirst({
389
- where: {
390
- id: ctx.user.id,
391
- schoolId: input.schoolId
392
- }
393
- });
394
- if (!user) {
395
- throw new TRPCError({ code: 'FORBIDDEN' });
396
- }
397
- return prisma.class.findMany({
398
- where: { schoolId: input.schoolId },
399
- include: {
400
- _count: {
401
- select: {
402
- students: true,
403
- teachers: true,
404
- assignments: true
405
- }
406
- }
407
- },
408
- orderBy: { name: 'asc' }
409
- });
410
- }),
411
- // Send bulk email
412
- sendBulkEmail: protectedProcedure
413
- .input(z.object({
414
- schoolId: z.string(),
415
- subject: z.string(),
416
- content: z.string(),
417
- recipientRole: z.enum(['ALL', 'STUDENT', 'TEACHER', 'ADMIN']).optional(),
418
- recipientIds: z.array(z.string()).optional()
419
- }))
420
- .mutation(async ({ input, ctx }) => {
421
- if (!ctx.user) {
422
- throw new TRPCError({
423
- code: 'UNAUTHORIZED',
424
- message: 'User must be authenticated'
425
- });
426
- }
427
- // Check admin permission
428
- const isAdmin = await prisma.user.findFirst({
429
- where: {
430
- id: ctx.user.id,
431
- schoolId: input.schoolId,
432
- role: UserRole.ADMIN
433
- }
434
- });
435
- if (!isAdmin) {
436
- throw new TRPCError({ code: 'FORBIDDEN' });
437
- }
438
- // Get recipients
439
- let recipients = [];
440
- if (input.recipientIds && input.recipientIds.length > 0) {
441
- recipients = await prisma.user.findMany({
442
- where: {
443
- id: { in: input.recipientIds },
444
- schoolId: input.schoolId
445
- },
446
- select: { email: true }
447
- });
448
- }
449
- else {
450
- const whereClause = { schoolId: input.schoolId };
451
- if (input.recipientRole && input.recipientRole !== 'ALL') {
452
- whereClause.role = input.recipientRole;
453
- }
454
- recipients = await prisma.user.findMany({
455
- where: whereClause,
456
- select: { email: true }
457
- });
458
- }
459
- // Send emails
460
- const results = [];
461
- for (const recipient of recipients) {
462
- try {
463
- await transport.sendMail({
464
- from: process.env.EMAIL_FROM || 'noreply@studious.app',
465
- to: recipient.email,
466
- subject: input.subject,
467
- text: input.content
468
- });
469
- results.push({ email: recipient.email, success: true });
470
- }
471
- catch (error) {
472
- results.push({
473
- email: recipient.email,
474
- success: false,
475
- error: error instanceof Error ? error.message : 'Unknown error'
476
- });
477
- }
478
- }
479
- return results;
480
- })
481
- });
482
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="59e27bf6-b975-511e-91ff-8378ed0fc1e4")}catch(e){}}();
483
- //# debugId=59e27bf6-b975-511e-91ff-8378ed0fc1e4