@studious-lms/server 1.0.6 → 1.0.8
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/API_SPECIFICATION.md +1461 -0
- package/dist/exportType.d.ts +3 -3
- package/dist/exportType.d.ts.map +1 -1
- package/dist/exportType.js +1 -2
- package/dist/index.js +25 -30
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +31 -29
- package/dist/lib/googleCloudStorage.js +9 -14
- package/dist/lib/prisma.js +4 -7
- package/dist/lib/thumbnailGenerator.js +12 -20
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +17 -22
- package/dist/middleware/logging.js +5 -9
- package/dist/routers/_app.d.ts +3619 -1937
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +28 -27
- package/dist/routers/agenda.d.ts +14 -9
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/agenda.js +14 -17
- package/dist/routers/announcement.d.ts +5 -4
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +28 -31
- package/dist/routers/assignment.d.ts +283 -197
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +256 -202
- package/dist/routers/attendance.d.ts +6 -5
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +31 -34
- package/dist/routers/auth.d.ts +2 -1
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +80 -75
- package/dist/routers/class.d.ts +285 -15
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +440 -164
- package/dist/routers/event.d.ts +48 -39
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/event.js +76 -79
- package/dist/routers/file.d.ts +72 -2
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +260 -32
- package/dist/routers/folder.d.ts +296 -0
- package/dist/routers/folder.d.ts.map +1 -0
- package/dist/routers/folder.js +693 -0
- package/dist/routers/notifications.d.ts +103 -0
- package/dist/routers/notifications.d.ts.map +1 -0
- package/dist/routers/notifications.js +91 -0
- package/dist/routers/school.d.ts +208 -0
- package/dist/routers/school.d.ts.map +1 -0
- package/dist/routers/school.js +481 -0
- package/dist/routers/section.d.ts +2 -1
- package/dist/routers/section.d.ts.map +1 -1
- package/dist/routers/section.js +30 -33
- package/dist/routers/user.d.ts +3 -2
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +21 -24
- package/dist/seedDatabase.d.ts +22 -0
- package/dist/seedDatabase.d.ts.map +1 -0
- package/dist/seedDatabase.js +75 -0
- package/dist/socket/handlers.js +26 -30
- package/dist/trpc.d.ts +5 -0
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +35 -26
- package/dist/types/trpc.d.ts +1 -1
- package/dist/types/trpc.d.ts.map +1 -1
- package/dist/types/trpc.js +1 -2
- package/dist/utils/email.js +2 -8
- package/dist/utils/generateInviteCode.js +1 -5
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +13 -9
- package/dist/utils/prismaErrorHandler.d.ts +9 -0
- package/dist/utils/prismaErrorHandler.d.ts.map +1 -0
- package/dist/utils/prismaErrorHandler.js +234 -0
- package/dist/utils/prismaWrapper.d.ts +14 -0
- package/dist/utils/prismaWrapper.d.ts.map +1 -0
- package/dist/utils/prismaWrapper.js +64 -0
- package/package.json +12 -4
- package/prisma/migrations/20250807062924_init/migration.sql +436 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +68 -1
- package/src/exportType.ts +3 -3
- package/src/index.ts +6 -6
- package/src/lib/fileUpload.ts +19 -10
- package/src/lib/thumbnailGenerator.ts +2 -2
- package/src/middleware/auth.ts +2 -4
- package/src/middleware/logging.ts +2 -2
- package/src/routers/_app.ts +17 -13
- package/src/routers/agenda.ts +2 -2
- package/src/routers/announcement.ts +2 -2
- package/src/routers/assignment.ts +86 -26
- package/src/routers/attendance.ts +2 -2
- package/src/routers/auth.ts +83 -57
- package/src/routers/class.ts +339 -39
- package/src/routers/event.ts +2 -2
- package/src/routers/file.ts +276 -21
- package/src/routers/folder.ts +755 -0
- package/src/routers/notifications.ts +93 -0
- package/src/routers/section.ts +2 -2
- package/src/routers/user.ts +3 -3
- package/src/seedDatabase.ts +88 -0
- package/src/socket/handlers.ts +5 -5
- package/src/trpc.ts +17 -4
- package/src/types/trpc.ts +1 -1
- package/src/utils/logger.ts +14 -4
- package/src/utils/prismaErrorHandler.ts +275 -0
- package/src/utils/prismaWrapper.ts +91 -0
- package/tests/auth.test.ts +25 -0
- package/tests/class.test.ts +281 -0
- package/tests/setup.ts +98 -0
- package/tests/startup.test.ts +5 -0
- package/tsconfig.json +2 -1
- package/vitest.config.ts +11 -0
- package/dist/logger.d.ts +0 -26
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -135
- package/src/logger.ts +0 -163
|
@@ -0,0 +1,481 @@
|
|
|
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
|
+
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const sectionRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
3
|
-
ctx: import("../trpc").Context;
|
|
3
|
+
ctx: import("../trpc.js").Context;
|
|
4
4
|
meta: object;
|
|
5
5
|
errorShape: {
|
|
6
6
|
data: {
|
|
7
7
|
zodError: z.typeToFlattenedError<any, string> | null;
|
|
8
|
+
prismaError: import("../utils/prismaErrorHandler.js").PrismaErrorInfo | null;
|
|
8
9
|
code: import("@trpc/server").TRPC_ERROR_CODE_KEY;
|
|
9
10
|
httpStatus: number;
|
|
10
11
|
path?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"section.d.ts","sourceRoot":"","sources":["../../src/routers/section.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAqBxB,eAAO,MAAM,aAAa
|
|
1
|
+
{"version":3,"file":"section.d.ts","sourceRoot":"","sources":["../../src/routers/section.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAqBxB,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoHxB,CAAC"}
|
package/dist/routers/section.js
CHANGED
|
@@ -1,35 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const createSectionSchema = zod_1.z.object({
|
|
9
|
-
classId: zod_1.z.string(),
|
|
10
|
-
name: zod_1.z.string(),
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
|
3
|
+
import { TRPCError } from "@trpc/server";
|
|
4
|
+
import { prisma } from "../lib/prisma.js";
|
|
5
|
+
const createSectionSchema = z.object({
|
|
6
|
+
classId: z.string(),
|
|
7
|
+
name: z.string(),
|
|
11
8
|
});
|
|
12
|
-
const updateSectionSchema =
|
|
13
|
-
id:
|
|
14
|
-
classId:
|
|
15
|
-
name:
|
|
9
|
+
const updateSectionSchema = z.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
classId: z.string(),
|
|
12
|
+
name: z.string(),
|
|
16
13
|
});
|
|
17
|
-
const deleteSectionSchema =
|
|
18
|
-
id:
|
|
19
|
-
classId:
|
|
14
|
+
const deleteSectionSchema = z.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
classId: z.string(),
|
|
20
17
|
});
|
|
21
|
-
|
|
22
|
-
create:
|
|
18
|
+
export const sectionRouter = createTRPCRouter({
|
|
19
|
+
create: protectedProcedure
|
|
23
20
|
.input(createSectionSchema)
|
|
24
21
|
.mutation(async ({ ctx, input }) => {
|
|
25
22
|
if (!ctx.user) {
|
|
26
|
-
throw new
|
|
23
|
+
throw new TRPCError({
|
|
27
24
|
code: "UNAUTHORIZED",
|
|
28
25
|
message: "User must be authenticated",
|
|
29
26
|
});
|
|
30
27
|
}
|
|
31
28
|
// Verify user is a teacher of the class
|
|
32
|
-
const classData = await
|
|
29
|
+
const classData = await prisma.class.findFirst({
|
|
33
30
|
where: {
|
|
34
31
|
id: input.classId,
|
|
35
32
|
teachers: {
|
|
@@ -40,12 +37,12 @@ exports.sectionRouter = (0, trpc_1.createTRPCRouter)({
|
|
|
40
37
|
},
|
|
41
38
|
});
|
|
42
39
|
if (!classData) {
|
|
43
|
-
throw new
|
|
40
|
+
throw new TRPCError({
|
|
44
41
|
code: "NOT_FOUND",
|
|
45
42
|
message: "Class not found or you are not a teacher",
|
|
46
43
|
});
|
|
47
44
|
}
|
|
48
|
-
const section = await
|
|
45
|
+
const section = await prisma.section.create({
|
|
49
46
|
data: {
|
|
50
47
|
name: input.name,
|
|
51
48
|
class: {
|
|
@@ -55,17 +52,17 @@ exports.sectionRouter = (0, trpc_1.createTRPCRouter)({
|
|
|
55
52
|
});
|
|
56
53
|
return section;
|
|
57
54
|
}),
|
|
58
|
-
update:
|
|
55
|
+
update: protectedProcedure
|
|
59
56
|
.input(updateSectionSchema)
|
|
60
57
|
.mutation(async ({ ctx, input }) => {
|
|
61
58
|
if (!ctx.user) {
|
|
62
|
-
throw new
|
|
59
|
+
throw new TRPCError({
|
|
63
60
|
code: "UNAUTHORIZED",
|
|
64
61
|
message: "User must be authenticated",
|
|
65
62
|
});
|
|
66
63
|
}
|
|
67
64
|
// Verify user is a teacher of the class
|
|
68
|
-
const classData = await
|
|
65
|
+
const classData = await prisma.class.findFirst({
|
|
69
66
|
where: {
|
|
70
67
|
id: input.classId,
|
|
71
68
|
teachers: {
|
|
@@ -76,12 +73,12 @@ exports.sectionRouter = (0, trpc_1.createTRPCRouter)({
|
|
|
76
73
|
},
|
|
77
74
|
});
|
|
78
75
|
if (!classData) {
|
|
79
|
-
throw new
|
|
76
|
+
throw new TRPCError({
|
|
80
77
|
code: "NOT_FOUND",
|
|
81
78
|
message: "Class not found or you are not a teacher",
|
|
82
79
|
});
|
|
83
80
|
}
|
|
84
|
-
const section = await
|
|
81
|
+
const section = await prisma.section.update({
|
|
85
82
|
where: { id: input.id },
|
|
86
83
|
data: {
|
|
87
84
|
name: input.name,
|
|
@@ -89,17 +86,17 @@ exports.sectionRouter = (0, trpc_1.createTRPCRouter)({
|
|
|
89
86
|
});
|
|
90
87
|
return section;
|
|
91
88
|
}),
|
|
92
|
-
delete:
|
|
89
|
+
delete: protectedProcedure
|
|
93
90
|
.input(deleteSectionSchema)
|
|
94
91
|
.mutation(async ({ ctx, input }) => {
|
|
95
92
|
if (!ctx.user) {
|
|
96
|
-
throw new
|
|
93
|
+
throw new TRPCError({
|
|
97
94
|
code: "UNAUTHORIZED",
|
|
98
95
|
message: "User must be authenticated",
|
|
99
96
|
});
|
|
100
97
|
}
|
|
101
98
|
// Verify user is a teacher of the class
|
|
102
|
-
const classData = await
|
|
99
|
+
const classData = await prisma.class.findFirst({
|
|
103
100
|
where: {
|
|
104
101
|
id: input.classId,
|
|
105
102
|
teachers: {
|
|
@@ -110,12 +107,12 @@ exports.sectionRouter = (0, trpc_1.createTRPCRouter)({
|
|
|
110
107
|
},
|
|
111
108
|
});
|
|
112
109
|
if (!classData) {
|
|
113
|
-
throw new
|
|
110
|
+
throw new TRPCError({
|
|
114
111
|
code: "NOT_FOUND",
|
|
115
112
|
message: "Class not found or you are not a teacher",
|
|
116
113
|
});
|
|
117
114
|
}
|
|
118
|
-
await
|
|
115
|
+
await prisma.section.delete({
|
|
119
116
|
where: { id: input.id },
|
|
120
117
|
});
|
|
121
118
|
return { id: input.id };
|
package/dist/routers/user.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const userRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
3
|
-
ctx: import("../trpc").Context;
|
|
3
|
+
ctx: import("../trpc.js").Context;
|
|
4
4
|
meta: object;
|
|
5
5
|
errorShape: {
|
|
6
6
|
data: {
|
|
7
7
|
zodError: z.typeToFlattenedError<any, string> | null;
|
|
8
|
+
prismaError: import("../utils/prismaErrorHandler.js").PrismaErrorInfo | null;
|
|
8
9
|
code: import("@trpc/server").TRPC_ERROR_CODE_KEY;
|
|
9
10
|
httpStatus: number;
|
|
10
11
|
path?: string;
|
|
@@ -33,8 +34,8 @@ export declare const userRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
33
34
|
profilePicture?: {
|
|
34
35
|
type: string;
|
|
35
36
|
name: string;
|
|
36
|
-
data: string;
|
|
37
37
|
size: number;
|
|
38
|
+
data: string;
|
|
38
39
|
} | undefined;
|
|
39
40
|
};
|
|
40
41
|
output: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/routers/user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAkBxB,eAAO,MAAM,UAAU
|
|
1
|
+
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/routers/user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAkBxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+DrB,CAAC"}
|