@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.
- package/.env.example +45 -0
- package/.env.test.example +37 -0
- package/README.md +34 -7
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +12110 -0
- package/coverage/coverage-final.json +44 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +221 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/server/index.html +116 -0
- package/coverage/server/src/exportType.ts.html +109 -0
- package/coverage/server/src/index.html +161 -0
- package/coverage/server/src/index.ts.html +1702 -0
- package/coverage/server/src/instrument.ts.html +130 -0
- package/coverage/server/src/lib/config/env.ts.html +448 -0
- package/coverage/server/src/lib/config/index.html +116 -0
- package/coverage/server/src/lib/fileUpload.ts.html +1138 -0
- package/coverage/server/src/lib/googleCloudStorage.ts.html +334 -0
- package/coverage/server/src/lib/index.html +206 -0
- package/coverage/server/src/lib/jsonConversion.ts.html +2323 -0
- package/coverage/server/src/lib/jsonStyles.ts.html +193 -0
- package/coverage/server/src/lib/notificationHandler.ts.html +193 -0
- package/coverage/server/src/lib/pusher.ts.html +121 -0
- package/coverage/server/src/lib/thumbnailGenerator.ts.html +592 -0
- package/coverage/server/src/middleware/auth.ts.html +646 -0
- package/coverage/server/src/middleware/index.html +146 -0
- package/coverage/server/src/middleware/logging.ts.html +244 -0
- package/coverage/server/src/middleware/security.ts.html +271 -0
- package/coverage/server/src/routers/_app.ts.html +232 -0
- package/coverage/server/src/routers/agenda.ts.html +319 -0
- package/coverage/server/src/routers/announcement.ts.html +3481 -0
- package/coverage/server/src/routers/assignment.ts.html +7633 -0
- package/coverage/server/src/routers/attendance.ts.html +1030 -0
- package/coverage/server/src/routers/auth.ts.html +1081 -0
- package/coverage/server/src/routers/class.ts.html +3535 -0
- package/coverage/server/src/routers/comment.ts.html +991 -0
- package/coverage/server/src/routers/conversation.ts.html +982 -0
- package/coverage/server/src/routers/event.ts.html +1609 -0
- package/coverage/server/src/routers/file.ts.html +1144 -0
- package/coverage/server/src/routers/folder.ts.html +2797 -0
- package/coverage/server/src/routers/index.html +386 -0
- package/coverage/server/src/routers/labChat.ts.html +3073 -0
- package/coverage/server/src/routers/marketing.ts.html +340 -0
- package/coverage/server/src/routers/message.ts.html +1912 -0
- package/coverage/server/src/routers/notifications.ts.html +364 -0
- package/coverage/server/src/routers/section.ts.html +1120 -0
- package/coverage/server/src/routers/user.ts.html +862 -0
- package/coverage/server/src/routers/worksheet.ts.html +1729 -0
- package/coverage/server/src/trpc.ts.html +397 -0
- package/coverage/server/src/types/index.html +116 -0
- package/coverage/server/src/types/trpc.ts.html +127 -0
- package/coverage/server/src/utils/aiUser.ts.html +280 -0
- package/coverage/server/src/utils/email.ts.html +121 -0
- package/coverage/server/src/utils/generateInviteCode.ts.html +106 -0
- package/coverage/server/src/utils/index.html +206 -0
- package/coverage/server/src/utils/inference.ts.html +709 -0
- package/coverage/server/src/utils/logger.ts.html +664 -0
- package/coverage/server/src/utils/prismaErrorHandler.ts.html +907 -0
- package/coverage/server/src/utils/prismaWrapper.ts.html +355 -0
- package/coverage/server/vitest.config.ts.html +196 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +83 -52
- package/dist/index.js.map +1 -1
- package/dist/instrument.js +15 -8
- package/dist/instrument.js.map +1 -1
- package/dist/lib/config/env.d.ts +169 -0
- package/dist/lib/config/env.d.ts.map +1 -0
- package/dist/lib/config/env.js +115 -0
- package/dist/lib/config/env.js.map +1 -0
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +5 -4
- package/dist/lib/fileUpload.js.map +1 -1
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +7 -8
- package/dist/lib/googleCloudStorage.js.map +1 -1
- package/dist/lib/jsonConversion.d.ts.map +1 -1
- package/dist/lib/jsonConversion.js +14 -16
- package/dist/lib/jsonConversion.js.map +1 -1
- package/dist/lib/notificationHandler.d.ts +2 -2
- package/dist/lib/prisma.d.ts +2 -2
- package/dist/lib/prisma.d.ts.map +1 -1
- package/dist/lib/prisma.js +22 -3
- package/dist/lib/prisma.js.map +1 -1
- package/dist/lib/pusher.d.ts.map +1 -1
- package/dist/lib/pusher.js +8 -7
- package/dist/lib/pusher.js.map +1 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +6 -5
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/security.d.ts +5 -0
- package/dist/middleware/security.d.ts.map +1 -0
- package/dist/middleware/security.js +77 -0
- package/dist/middleware/security.js.map +1 -0
- package/dist/routers/_app.d.ts +304 -98
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/agenda.js +12 -9
- package/dist/routers/agenda.js.map +1 -1
- package/dist/routers/announcement.d.ts +8 -0
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +6 -4
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +7 -4
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +35 -18
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/attendance.d.ts +1 -0
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +4 -4
- package/dist/routers/attendance.js.map +1 -1
- package/dist/routers/auth.d.ts +20 -0
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +132 -15
- package/dist/routers/auth.js.map +1 -1
- package/dist/routers/class.d.ts +10 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +49 -5
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/comment.d.ts +7 -0
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/comment.js +9 -2
- package/dist/routers/comment.js.map +1 -1
- package/dist/routers/conversation.d.ts +1 -0
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +46 -31
- package/dist/routers/conversation.js.map +1 -1
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +30 -7
- package/dist/routers/file.js.map +1 -1
- package/dist/routers/labChat.d.ts +1 -0
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +2 -3
- package/dist/routers/labChat.js.map +1 -1
- package/dist/routers/marketing.d.ts +1 -1
- package/dist/routers/newtonChat.d.ts +55 -0
- package/dist/routers/newtonChat.d.ts.map +1 -0
- package/dist/routers/newtonChat.js +438 -0
- package/dist/routers/newtonChat.js.map +1 -0
- package/dist/routers/notifications.d.ts +4 -4
- package/dist/routers/section.d.ts +9 -4
- package/dist/routers/section.d.ts.map +1 -1
- package/dist/routers/section.js +8 -8
- package/dist/routers/section.js.map +1 -1
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +5 -4
- package/dist/routers/user.js.map +1 -1
- package/dist/routers/worksheet.d.ts +30 -36
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +11 -33
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/seedDatabase.d.ts +1 -1
- package/dist/seedDatabase.js +275 -284
- package/dist/seedDatabase.js.map +1 -1
- package/dist/server/pipelines/aiLabChat.d.ts +10 -0
- package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
- package/dist/server/pipelines/aiLabChat.js +83 -0
- package/dist/server/pipelines/aiLabChat.js.map +1 -0
- package/dist/server/pipelines/gradeWorksheet.d.ts +2 -0
- package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
- package/dist/server/pipelines/gradeWorksheet.js +138 -0
- package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +2 -2
- package/dist/trpc.js.map +1 -1
- package/dist/utils/email.d.ts +9 -1
- package/dist/utils/email.d.ts.map +1 -1
- package/dist/utils/email.js +20 -5
- package/dist/utils/email.js.map +1 -1
- package/dist/utils/inference.d.ts +3 -0
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +41 -7
- package/dist/utils/inference.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +3 -3
- package/dist/utils/logger.js.map +1 -1
- package/docker-compose.yml +14 -0
- package/package.json +13 -4
- package/prisma/schema.prisma +32 -5
- package/scripts/test-pre-push.ts +14 -0
- package/src/index.ts +98 -54
- package/src/instrument.ts +13 -6
- package/src/lib/config/env.ts +126 -0
- package/src/lib/fileUpload.ts +3 -2
- package/src/lib/googleCloudStorage.ts +6 -6
- package/src/lib/jsonConversion.ts +12 -14
- package/src/lib/prisma.ts +23 -2
- package/src/lib/pusher.ts +6 -5
- package/src/middleware/auth.ts +4 -3
- package/src/middleware/security.ts +80 -0
- package/src/routers/_app.ts +2 -0
- package/src/routers/agenda.ts +10 -7
- package/src/routers/announcement.ts +4 -2
- package/src/routers/assignment.ts +58 -40
- package/src/routers/attendance.ts +2 -2
- package/src/routers/auth.ts +143 -14
- package/src/routers/class.ts +52 -3
- package/src/routers/comment.ts +7 -0
- package/src/routers/conversation.ts +49 -29
- package/src/routers/file.ts +29 -5
- package/src/routers/labChat.ts +0 -1
- package/src/routers/newtonChat.ts +520 -0
- package/src/routers/section.ts +6 -6
- package/src/routers/user.ts +3 -2
- package/src/routers/worksheet.ts +9 -37
- package/src/seedDatabase.ts +290 -283
- package/src/server/pipelines/aiLabChat.ts +92 -0
- package/src/server/pipelines/gradeWorksheet.ts +152 -0
- package/src/trpc.ts +2 -0
- package/src/utils/email.ts +30 -3
- package/src/utils/inference.ts +50 -5
- package/src/utils/logger.ts +2 -1
- package/tests/announcement.test.ts +164 -0
- package/tests/assignment.test.ts +296 -0
- package/tests/attendance.test.ts +168 -0
- package/tests/auth.test.ts +33 -10
- package/tests/class.test.ts +34 -9
- package/tests/event.test.ts +228 -0
- package/tests/section.test.ts +216 -0
- package/tests/setup.ts +70 -16
- package/tests/user.test.ts +158 -0
- package/vitest.config.ts +26 -0
- package/API_SPECIFICATION.md +0 -1597
- package/BASE64_REMOVAL_SUMMARY.md +0 -164
- package/CHAT_API_SPEC.md +0 -579
- package/LAB_CHAT_API_SPEC.md +0 -518
- package/dist/routers/school.d.ts +0 -208
- package/dist/routers/school.d.ts.map +0 -1
- package/dist/routers/school.js +0 -483
package/src/routers/auth.ts
CHANGED
|
@@ -4,8 +4,10 @@ import { TRPCError } from "@trpc/server";
|
|
|
4
4
|
import { prisma } from "../lib/prisma.js";
|
|
5
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
6
6
|
import { compare, hash } from "bcryptjs";
|
|
7
|
-
import {
|
|
7
|
+
import { sendMail } from "../utils/email.js";
|
|
8
8
|
import { prismaWrapper } from "../utils/prismaWrapper.js";
|
|
9
|
+
import { env } from "../lib/config/env.js";
|
|
10
|
+
import { logger } from "../utils/logger.js";
|
|
9
11
|
|
|
10
12
|
const loginSchema = z.object({
|
|
11
13
|
username: z.string(),
|
|
@@ -106,14 +108,18 @@ export const authRouter = createTRPCRouter({
|
|
|
106
108
|
'creating verification token'
|
|
107
109
|
);
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
try {
|
|
112
|
+
await sendMail({
|
|
113
|
+
from: 'noreply@studious.sh',
|
|
114
|
+
to: user.email,
|
|
115
|
+
subject: 'Verify your email',
|
|
116
|
+
text: `Click the link to verify your email: ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`,
|
|
117
|
+
});
|
|
118
|
+
} catch (err) {
|
|
119
|
+
logger.error('Failed to send verification email', { email: user.email, err });
|
|
120
|
+
}
|
|
115
121
|
|
|
116
|
-
|
|
122
|
+
// logger.info(`Password verification email sent to ${user.email} at ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`);
|
|
117
123
|
|
|
118
124
|
return {
|
|
119
125
|
user: {
|
|
@@ -280,12 +286,18 @@ export const authRouter = createTRPCRouter({
|
|
|
280
286
|
},
|
|
281
287
|
});
|
|
282
288
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
+
try {
|
|
290
|
+
await sendMail({
|
|
291
|
+
from: 'noreply@studious.sh',
|
|
292
|
+
to: user.email,
|
|
293
|
+
subject: 'Verify your email',
|
|
294
|
+
text: `Click the link to verify your email: ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`,
|
|
295
|
+
});
|
|
296
|
+
} catch (err) {
|
|
297
|
+
logger.error('Failed to send verification email', { email: user.email, err });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// logger.info(`Password verification email sent to ${user.email} at ${env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`);
|
|
289
301
|
|
|
290
302
|
return { success: true };
|
|
291
303
|
}),
|
|
@@ -326,6 +338,123 @@ export const authRouter = createTRPCRouter({
|
|
|
326
338
|
where: { id: token },
|
|
327
339
|
});
|
|
328
340
|
|
|
341
|
+
return { success: true };
|
|
342
|
+
}),
|
|
343
|
+
|
|
344
|
+
requestPasswordReset: publicProcedure
|
|
345
|
+
.input(z.object({
|
|
346
|
+
email: z.string().email(),
|
|
347
|
+
}))
|
|
348
|
+
.mutation(async ({ input }) => {
|
|
349
|
+
const { email } = input;
|
|
350
|
+
|
|
351
|
+
const user = await prisma.user.findFirst({
|
|
352
|
+
where: { email },
|
|
353
|
+
select: {
|
|
354
|
+
id: true,
|
|
355
|
+
email: true,
|
|
356
|
+
username: true,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Don't reveal if user exists or not for security
|
|
361
|
+
if (!user) {
|
|
362
|
+
return { success: true };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Delete any existing password reset tokens for this user
|
|
366
|
+
// Only delete tokens that expire within 2 hours (likely password reset tokens)
|
|
367
|
+
const twoHoursFromNow = new Date(Date.now() + 1000 * 60 * 60 * 2);
|
|
368
|
+
await prisma.session.deleteMany({
|
|
369
|
+
where: {
|
|
370
|
+
userId: user.id,
|
|
371
|
+
classId: null,
|
|
372
|
+
expiresAt: {
|
|
373
|
+
lte: twoHoursFromNow, // Only delete short-lived tokens (password reset tokens)
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Create a new password reset token (expires in 1 hour)
|
|
379
|
+
const resetToken = await prisma.session.create({
|
|
380
|
+
data: {
|
|
381
|
+
id: uuidv4(),
|
|
382
|
+
userId: user.id,
|
|
383
|
+
expiresAt: new Date(Date.now() + 1000 * 60 * 60), // 1 hour
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Send password reset email
|
|
388
|
+
try {
|
|
389
|
+
await sendMail({
|
|
390
|
+
from: 'noreply@studious.sh',
|
|
391
|
+
to: user.email,
|
|
392
|
+
subject: 'Reset your password',
|
|
393
|
+
text: `Click the link to reset your password: ${env.NEXT_PUBLIC_APP_URL}/reset-password/${resetToken.id}`,
|
|
394
|
+
});
|
|
395
|
+
} catch (err) {
|
|
396
|
+
logger.error('Failed to send password reset email', { email: user.email, err });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// logger.info(`Password reset email sent to ${user.email} at ${env.NEXT_PUBLIC_APP_URL}/reset-password/${resetToken.id}`);
|
|
400
|
+
|
|
401
|
+
return { success: true };
|
|
402
|
+
}),
|
|
403
|
+
|
|
404
|
+
resetPassword: publicProcedure
|
|
405
|
+
.input(z.object({
|
|
406
|
+
token: z.string(),
|
|
407
|
+
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
408
|
+
confirmPassword: z.string(),
|
|
409
|
+
}).refine((data) => data.password === data.confirmPassword, {
|
|
410
|
+
message: "Passwords don't match",
|
|
411
|
+
path: ["confirmPassword"],
|
|
412
|
+
}))
|
|
413
|
+
.mutation(async ({ input }) => {
|
|
414
|
+
const { token, password } = input;
|
|
415
|
+
|
|
416
|
+
const session = await prisma.session.findUnique({
|
|
417
|
+
where: { id: token },
|
|
418
|
+
include: {
|
|
419
|
+
user: {
|
|
420
|
+
select: {
|
|
421
|
+
id: true,
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (!session || !session.userId) {
|
|
428
|
+
throw new TRPCError({
|
|
429
|
+
code: "NOT_FOUND",
|
|
430
|
+
message: "Invalid or expired reset token",
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (session.expiresAt && session.expiresAt < new Date()) {
|
|
435
|
+
// Clean up expired token
|
|
436
|
+
await prisma.session.delete({
|
|
437
|
+
where: { id: token },
|
|
438
|
+
});
|
|
439
|
+
throw new TRPCError({
|
|
440
|
+
code: "UNAUTHORIZED",
|
|
441
|
+
message: "Reset token has expired",
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Update the user's password
|
|
446
|
+
await prisma.user.update({
|
|
447
|
+
where: { id: session.userId },
|
|
448
|
+
data: {
|
|
449
|
+
password: await hash(password, 10),
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Clean up the reset token
|
|
454
|
+
await prisma.session.delete({
|
|
455
|
+
where: { id: token },
|
|
456
|
+
});
|
|
457
|
+
|
|
329
458
|
return { success: true };
|
|
330
459
|
}),
|
|
331
460
|
});
|
package/src/routers/class.ts
CHANGED
|
@@ -465,6 +465,51 @@ export const classRouter = createTRPCRouter({
|
|
|
465
465
|
removedUserId: userId,
|
|
466
466
|
};
|
|
467
467
|
}),
|
|
468
|
+
leaveClass: protectedProcedure
|
|
469
|
+
.input(z.object({
|
|
470
|
+
classId: z.string(),
|
|
471
|
+
}))
|
|
472
|
+
.mutation(async ({ ctx, input }) => {
|
|
473
|
+
const { classId } = input;
|
|
474
|
+
const userId = ctx.user?.id;
|
|
475
|
+
|
|
476
|
+
if (!userId) {
|
|
477
|
+
throw new TRPCError({
|
|
478
|
+
code: 'UNAUTHORIZED',
|
|
479
|
+
message: 'User not authenticated',
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const classData = await prisma.class.findFirst({
|
|
484
|
+
where: {
|
|
485
|
+
id: classId,
|
|
486
|
+
students: {
|
|
487
|
+
some: { id: userId },
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (!classData) {
|
|
493
|
+
throw new TRPCError({
|
|
494
|
+
code: 'NOT_FOUND',
|
|
495
|
+
message: 'Class not found or you are not a student in this class',
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
await prisma.class.update({
|
|
500
|
+
where: { id: classId },
|
|
501
|
+
data: {
|
|
502
|
+
students: {
|
|
503
|
+
disconnect: { id: userId },
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
success: true,
|
|
510
|
+
leftClassId: classId,
|
|
511
|
+
};
|
|
512
|
+
}),
|
|
468
513
|
join: protectedProcedure
|
|
469
514
|
.input(z.object({
|
|
470
515
|
classCode: z.string(),
|
|
@@ -472,9 +517,13 @@ export const classRouter = createTRPCRouter({
|
|
|
472
517
|
.mutation(async ({ ctx, input }) => {
|
|
473
518
|
const { classCode } = input;
|
|
474
519
|
|
|
520
|
+
// Case-insensitive search for invite code
|
|
475
521
|
const session = await prisma.session.findFirst({
|
|
476
522
|
where: {
|
|
477
|
-
id:
|
|
523
|
+
id: {
|
|
524
|
+
equals: classCode,
|
|
525
|
+
mode: 'insensitive',
|
|
526
|
+
},
|
|
478
527
|
},
|
|
479
528
|
});
|
|
480
529
|
|
|
@@ -676,7 +725,7 @@ export const classRouter = createTRPCRouter({
|
|
|
676
725
|
|
|
677
726
|
return events;
|
|
678
727
|
}),
|
|
679
|
-
listMarkSchemes:
|
|
728
|
+
listMarkSchemes: protectedClassMemberProcedure
|
|
680
729
|
.input(z.object({
|
|
681
730
|
classId: z.string(),
|
|
682
731
|
}))
|
|
@@ -755,7 +804,7 @@ export const classRouter = createTRPCRouter({
|
|
|
755
804
|
|
|
756
805
|
return markScheme;
|
|
757
806
|
}),
|
|
758
|
-
listGradingBoundaries:
|
|
807
|
+
listGradingBoundaries: protectedClassMemberProcedure
|
|
759
808
|
.input(z.object({
|
|
760
809
|
classId: z.string(),
|
|
761
810
|
}))
|
package/src/routers/comment.ts
CHANGED
|
@@ -148,51 +148,69 @@ export const conversationRouter = createTRPCRouter({
|
|
|
148
148
|
|
|
149
149
|
// For DMs, check if conversation already exists
|
|
150
150
|
if (type === 'DM') {
|
|
151
|
-
|
|
151
|
+
// Get the target user's ID from their username
|
|
152
|
+
const targetUser = await prisma.user.findFirst({
|
|
153
|
+
where: { username: memberIds[0] },
|
|
154
|
+
select: { id: true, username: true },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (!targetUser) {
|
|
158
|
+
throw new TRPCError({
|
|
159
|
+
code: 'BAD_REQUEST',
|
|
160
|
+
message: `User "${memberIds[0]}" not found`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Find all DM conversations where current user is a member
|
|
165
|
+
const existingDMs = await prisma.conversation.findMany({
|
|
152
166
|
where: {
|
|
153
167
|
type: 'DM',
|
|
154
168
|
members: {
|
|
155
|
-
|
|
156
|
-
userId
|
|
157
|
-
in: [userId, memberIds[0]],
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
AND: {
|
|
162
|
-
members: {
|
|
163
|
-
some: {
|
|
164
|
-
userId,
|
|
165
|
-
},
|
|
169
|
+
some: {
|
|
170
|
+
userId,
|
|
166
171
|
},
|
|
167
172
|
},
|
|
168
173
|
},
|
|
169
174
|
include: {
|
|
170
175
|
members: {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
select: {
|
|
174
|
-
id: true,
|
|
175
|
-
username: true,
|
|
176
|
-
profile: {
|
|
177
|
-
select: {
|
|
178
|
-
displayName: true,
|
|
179
|
-
profilePicture: true,
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
},
|
|
176
|
+
select: {
|
|
177
|
+
userId: true,
|
|
184
178
|
},
|
|
185
179
|
},
|
|
186
180
|
},
|
|
187
181
|
});
|
|
188
182
|
|
|
183
|
+
// Check if any of these conversations has exactly 2 members (current user + target user)
|
|
184
|
+
const existingDM = existingDMs.find(conv => {
|
|
185
|
+
const memberUserIds = conv.members.map(m => m.userId);
|
|
186
|
+
return memberUserIds.length === 2 &&
|
|
187
|
+
memberUserIds.includes(userId) &&
|
|
188
|
+
memberUserIds.includes(targetUser.id);
|
|
189
|
+
});
|
|
190
|
+
|
|
189
191
|
if (existingDM) {
|
|
190
|
-
|
|
192
|
+
// Conversation already exists, throw error with friendly message
|
|
193
|
+
throw new TRPCError({
|
|
194
|
+
code: 'BAD_REQUEST',
|
|
195
|
+
message: `A conversation with ${targetUser.username} already exists`,
|
|
196
|
+
});
|
|
191
197
|
}
|
|
192
198
|
}
|
|
193
199
|
|
|
194
200
|
// Verify all members exist
|
|
195
|
-
const
|
|
201
|
+
const membersWithIds = await prisma.user.findMany({
|
|
202
|
+
where: {
|
|
203
|
+
id: {
|
|
204
|
+
in: memberIds,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
select: {
|
|
208
|
+
id: true,
|
|
209
|
+
username: true,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const membersWithUsernames = await prisma.user.findMany({
|
|
196
214
|
where: {
|
|
197
215
|
username: {
|
|
198
216
|
in: memberIds,
|
|
@@ -204,6 +222,8 @@ export const conversationRouter = createTRPCRouter({
|
|
|
204
222
|
},
|
|
205
223
|
});
|
|
206
224
|
|
|
225
|
+
const members = [...membersWithIds, ...membersWithUsernames];
|
|
226
|
+
|
|
207
227
|
if (members.length !== memberIds.length) {
|
|
208
228
|
throw new TRPCError({
|
|
209
229
|
code: 'BAD_REQUEST',
|
|
@@ -222,8 +242,8 @@ export const conversationRouter = createTRPCRouter({
|
|
|
222
242
|
userId,
|
|
223
243
|
role: type === 'GROUP' ? 'ADMIN' : 'MEMBER',
|
|
224
244
|
},
|
|
225
|
-
...
|
|
226
|
-
userId:
|
|
245
|
+
...members.map((member) => ({
|
|
246
|
+
userId: member.id,
|
|
227
247
|
role: 'MEMBER' as const,
|
|
228
248
|
})),
|
|
229
249
|
],
|
package/src/routers/file.ts
CHANGED
|
@@ -73,6 +73,16 @@ export const fileRouter = createTRPCRouter({
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
+
},
|
|
77
|
+
announcement: {
|
|
78
|
+
include: {
|
|
79
|
+
class: {
|
|
80
|
+
include: {
|
|
81
|
+
students: true,
|
|
82
|
+
teachers: true
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
76
86
|
}
|
|
77
87
|
}
|
|
78
88
|
});
|
|
@@ -92,18 +102,30 @@ export const fileRouter = createTRPCRouter({
|
|
|
92
102
|
// Check if user is a teacher of the class
|
|
93
103
|
if (file.assignment?.class) {
|
|
94
104
|
classId = file.assignment.class.id;
|
|
95
|
-
|
|
105
|
+
const isTeacher = file.assignment.class.teachers.some(teacher => teacher.id === userId);
|
|
106
|
+
const isStudent = file.assignment.class.students.some(student => student.id === userId);
|
|
107
|
+
logger.info(`Assignment file access check - userId: ${userId}, isTeacher: ${isTeacher}, isStudent: ${isStudent}`);
|
|
108
|
+
hasAccess = isTeacher || isStudent;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check if user has access to announcement files (teachers or students in the class)
|
|
112
|
+
if ((file as any).announcement?.class) {
|
|
113
|
+
classId = (file as any).announcement.class.id;
|
|
114
|
+
const isTeacher = (file as any).announcement.class.teachers.some((teacher: any) => teacher.id === userId);
|
|
115
|
+
const isStudent = (file as any).announcement.class.students.some((student: any) => student.id === userId);
|
|
116
|
+
logger.info(`Announcement file access check - userId: ${userId}, isTeacher: ${isTeacher}, isStudent: ${isStudent}`);
|
|
117
|
+
hasAccess = hasAccess || isTeacher || isStudent;
|
|
96
118
|
}
|
|
97
119
|
|
|
98
120
|
if (file.submission?.assignment?.classId) {
|
|
99
121
|
classId = file.submission.assignment.classId;
|
|
100
|
-
hasAccess = file.submission?.studentId === userId || false;
|
|
122
|
+
hasAccess = hasAccess || file.submission?.studentId === userId || false;
|
|
101
123
|
if (!hasAccess) hasAccess = file.submission.assignment.class.teachers.some(teacher => teacher.id === userId) || false;
|
|
102
124
|
}
|
|
103
125
|
|
|
104
126
|
if (file.annotations?.assignment?.classId) {
|
|
105
127
|
classId = file.annotations?.assignment.classId;
|
|
106
|
-
hasAccess = file.annotations?.studentId === userId || false;
|
|
128
|
+
hasAccess = hasAccess || file.annotations?.studentId === userId || false;
|
|
107
129
|
if (!hasAccess) hasAccess = file.annotations.assignment.class.teachers.some(teacher => teacher.id === userId) || false;
|
|
108
130
|
}
|
|
109
131
|
|
|
@@ -114,8 +136,10 @@ export const fileRouter = createTRPCRouter({
|
|
|
114
136
|
|
|
115
137
|
// Check if file is in a folder and user has access to the class
|
|
116
138
|
if (file.folder?.class) {
|
|
117
|
-
|
|
118
|
-
|
|
139
|
+
const isTeacher = file.folder.class.teachers.some(teacher => teacher.id === userId);
|
|
140
|
+
const isStudent = file.folder.class.students.some(student => student.id === userId);
|
|
141
|
+
hasAccess = hasAccess || isTeacher;
|
|
142
|
+
hasAccess = hasAccess || isStudent;
|
|
119
143
|
}
|
|
120
144
|
|
|
121
145
|
if (!hasAccess) {
|
package/src/routers/labChat.ts
CHANGED
|
@@ -902,7 +902,6 @@ WHEN CREATING COURSE MATERIALS (docs field):
|
|
|
902
902
|
if (jsonData.docs && Array.isArray(jsonData.docs)) {
|
|
903
903
|
|
|
904
904
|
|
|
905
|
-
console.log('Generating PDFs', { labChatId, docs: JSON.stringify(jsonData.docs, null, 2) });
|
|
906
905
|
for (let i = 0; i < jsonData.docs.length; i++) {
|
|
907
906
|
const doc = jsonData.docs[i];
|
|
908
907
|
if (!doc.title || !doc.blocks || !Array.isArray(doc.blocks)) {
|