@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/dist/routers/school.js
DELETED
|
@@ -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
|