@studious-lms/server 1.0.1 → 1.0.3

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 (66) hide show
  1. package/dist/index.js +4 -4
  2. package/dist/middleware/auth.js +1 -1
  3. package/dist/middleware/logging.js +1 -1
  4. package/dist/routers/_app.js +1 -1
  5. package/dist/routers/agenda.js +1 -1
  6. package/dist/routers/announcement.js +1 -1
  7. package/dist/routers/assignment.js +3 -3
  8. package/dist/routers/attendance.js +1 -1
  9. package/dist/routers/auth.js +2 -2
  10. package/dist/routers/class.js +2 -2
  11. package/dist/routers/event.js +1 -1
  12. package/dist/routers/file.js +2 -2
  13. package/dist/routers/section.js +1 -1
  14. package/dist/routers/user.js +2 -2
  15. package/dist/trpc.js +2 -2
  16. package/package.json +1 -6
  17. package/prisma/schema.prisma +228 -0
  18. package/src/exportType.ts +9 -0
  19. package/src/index.ts +94 -0
  20. package/src/lib/fileUpload.ts +163 -0
  21. package/src/lib/googleCloudStorage.ts +94 -0
  22. package/src/lib/prisma.ts +16 -0
  23. package/src/lib/thumbnailGenerator.ts +185 -0
  24. package/src/logger.ts +163 -0
  25. package/src/middleware/auth.ts +191 -0
  26. package/src/middleware/logging.ts +54 -0
  27. package/src/routers/_app.ts +34 -0
  28. package/src/routers/agenda.ts +79 -0
  29. package/src/routers/announcement.ts +134 -0
  30. package/src/routers/assignment.ts +1614 -0
  31. package/src/routers/attendance.ts +284 -0
  32. package/src/routers/auth.ts +286 -0
  33. package/src/routers/class.ts +753 -0
  34. package/src/routers/event.ts +509 -0
  35. package/src/routers/file.ts +96 -0
  36. package/src/routers/section.ts +138 -0
  37. package/src/routers/user.ts +82 -0
  38. package/src/socket/handlers.ts +143 -0
  39. package/src/trpc.ts +90 -0
  40. package/src/types/trpc.ts +15 -0
  41. package/src/utils/email.ts +11 -0
  42. package/src/utils/generateInviteCode.ts +8 -0
  43. package/src/utils/logger.ts +156 -0
  44. package/tsconfig.json +17 -0
  45. package/generated/prisma/client.d.ts +0 -1
  46. package/generated/prisma/client.js +0 -4
  47. package/generated/prisma/default.d.ts +0 -1
  48. package/generated/prisma/default.js +0 -4
  49. package/generated/prisma/edge.d.ts +0 -1
  50. package/generated/prisma/edge.js +0 -389
  51. package/generated/prisma/index-browser.js +0 -375
  52. package/generated/prisma/index.d.ts +0 -34865
  53. package/generated/prisma/index.js +0 -410
  54. package/generated/prisma/libquery_engine-darwin-arm64.dylib.node +0 -0
  55. package/generated/prisma/package.json +0 -140
  56. package/generated/prisma/runtime/edge-esm.js +0 -34
  57. package/generated/prisma/runtime/edge.js +0 -34
  58. package/generated/prisma/runtime/index-browser.d.ts +0 -370
  59. package/generated/prisma/runtime/index-browser.js +0 -16
  60. package/generated/prisma/runtime/library.d.ts +0 -3647
  61. package/generated/prisma/runtime/library.js +0 -146
  62. package/generated/prisma/runtime/react-native.js +0 -83
  63. package/generated/prisma/runtime/wasm.js +0 -35
  64. package/generated/prisma/schema.prisma +0 -304
  65. package/generated/prisma/wasm.d.ts +0 -1
  66. package/generated/prisma/wasm.js +0 -375
@@ -0,0 +1,284 @@
1
+ import { z } from "zod";
2
+ import { createTRPCRouter, protectedProcedure } from "../trpc";
3
+ import { TRPCError } from "@trpc/server";
4
+ import { prisma } from "../lib/prisma";
5
+
6
+ const attendanceSchema = z.object({
7
+ eventId: z.string().optional(),
8
+ present: z.array(z.object({ id: z.string(), username: z.string() })),
9
+ late: z.array(z.object({ id: z.string(), username: z.string() })),
10
+ absent: z.array(z.object({ id: z.string(), username: z.string() })),
11
+ });
12
+
13
+ export const attendanceRouter = createTRPCRouter({
14
+ get: protectedProcedure
15
+ .input(z.object({
16
+ classId: z.string(),
17
+ eventId: z.string().optional(),
18
+ }))
19
+ .query(async ({ ctx, input }) => {
20
+ if (!ctx.user) {
21
+ throw new TRPCError({
22
+ code: "UNAUTHORIZED",
23
+ message: "You must be logged in to view attendance",
24
+ });
25
+ }
26
+
27
+ // Check if user is a teacher or student of the class
28
+ const classData = await prisma.class.findUnique({
29
+ where: {
30
+ id: input.classId,
31
+ OR: [
32
+ {
33
+ teachers: {
34
+ some: {
35
+ id: ctx.user.id,
36
+ },
37
+ },
38
+ },
39
+ {
40
+ students: {
41
+ some: {
42
+ id: ctx.user.id,
43
+ },
44
+ },
45
+ },
46
+ ],
47
+ },
48
+ select: {
49
+ students: {
50
+ select: {
51
+ id: true,
52
+ },
53
+ },
54
+ },
55
+ });
56
+
57
+ if (!classData) {
58
+ throw new TRPCError({
59
+ code: "UNAUTHORIZED",
60
+ message: "You are not authorized to view this class's attendance",
61
+ });
62
+ }
63
+
64
+ // check each event has an attendance, if not create one
65
+ const events = await prisma.event.findMany({
66
+ where: {
67
+ classId: input.classId,
68
+ },
69
+ });
70
+
71
+ for (const event of events) {
72
+ const attendance = await prisma.attendance.findFirst({
73
+ where: {
74
+ eventId: event.id,
75
+ },
76
+ });
77
+
78
+ if (!attendance) {
79
+ await prisma.attendance.create({
80
+ data: {
81
+ event: {
82
+ connect: {
83
+ id: event.id,
84
+ },
85
+ },
86
+ class: {
87
+ connect: {
88
+ id: input.classId,
89
+ },
90
+ },
91
+ present: {
92
+ connect: classData.students.map(student => ({ id: student.id })),
93
+ },
94
+ },
95
+ });
96
+ }
97
+ }
98
+
99
+
100
+ const attendance = await prisma.attendance.findMany({
101
+ where: {
102
+ classId: input.classId,
103
+ ...(input.eventId ? { eventId: input.eventId } : {}),
104
+ },
105
+ include: {
106
+ event: {
107
+ select: {
108
+ id: true,
109
+ name: true,
110
+ startTime: true,
111
+ endTime: true,
112
+ location: true,
113
+ color: true,
114
+ },
115
+ },
116
+ present: {
117
+ select: {
118
+ id: true,
119
+ username: true,
120
+ },
121
+ },
122
+ late: {
123
+ select: {
124
+ id: true,
125
+ username: true,
126
+ },
127
+ },
128
+ absent: {
129
+ select: {
130
+ id: true,
131
+ username: true,
132
+ },
133
+ },
134
+ },
135
+ orderBy: {
136
+ date: "desc",
137
+ },
138
+ });
139
+
140
+ return attendance;
141
+ }),
142
+
143
+ update: protectedProcedure
144
+ .input(z.object({
145
+ classId: z.string(),
146
+ eventId: z.string().optional(),
147
+ attendance: attendanceSchema,
148
+ }))
149
+ .mutation(async ({ ctx, input }) => {
150
+ if (!ctx.user) {
151
+ throw new TRPCError({
152
+ code: "UNAUTHORIZED",
153
+ message: "You must be logged in to update attendance",
154
+ });
155
+ }
156
+
157
+ // Check if user is a teacher of the class
158
+ const classData = await prisma.class.findUnique({
159
+ where: {
160
+ id: input.classId,
161
+ teachers: {
162
+ some: {
163
+ id: ctx.user.id,
164
+ },
165
+ },
166
+ },
167
+ });
168
+
169
+ if (!classData) {
170
+ throw new TRPCError({
171
+ code: "UNAUTHORIZED",
172
+ message: "You are not authorized to update this class's attendance",
173
+ });
174
+ }
175
+
176
+ // Check if attendance record exists
177
+ const existingAttendance = await prisma.attendance.findFirst({
178
+ where: {
179
+ classId: input.classId,
180
+ eventId: input.eventId,
181
+ },
182
+ });
183
+
184
+ if (!existingAttendance) {
185
+ // Create new attendance record
186
+ const attendance = await prisma.attendance.create({
187
+ data: {
188
+ classId: input.classId,
189
+ eventId: input.eventId,
190
+ date: new Date(),
191
+ present: {
192
+ connect: input.attendance.present.map(student => ({ id: student.id })),
193
+ },
194
+ late: {
195
+ connect: input.attendance.late.map(student => ({ id: student.id })),
196
+ },
197
+ absent: {
198
+ connect: input.attendance.absent.map(student => ({ id: student.id })),
199
+ },
200
+ },
201
+ include: {
202
+ event: {
203
+ select: {
204
+ id: true,
205
+ name: true,
206
+ startTime: true,
207
+ endTime: true,
208
+ location: true,
209
+ },
210
+ },
211
+ present: {
212
+ select: {
213
+ id: true,
214
+ username: true,
215
+ },
216
+ },
217
+ late: {
218
+ select: {
219
+ id: true,
220
+ username: true,
221
+ },
222
+ },
223
+ absent: {
224
+ select: {
225
+ id: true,
226
+ username: true,
227
+ },
228
+ },
229
+ },
230
+ });
231
+
232
+ return attendance;
233
+ }
234
+
235
+ // Update existing attendance record
236
+ const attendance = await prisma.attendance.update({
237
+ where: {
238
+ id: existingAttendance.id,
239
+ },
240
+ data: {
241
+ present: {
242
+ set: input.attendance.present.map(student => ({ id: student.id })),
243
+ },
244
+ late: {
245
+ set: input.attendance.late.map(student => ({ id: student.id })),
246
+ },
247
+ absent: {
248
+ set: input.attendance.absent.map(student => ({ id: student.id })),
249
+ },
250
+ },
251
+ include: {
252
+ event: {
253
+ select: {
254
+ id: true,
255
+ name: true,
256
+ startTime: true,
257
+ endTime: true,
258
+ location: true,
259
+ },
260
+ },
261
+ present: {
262
+ select: {
263
+ id: true,
264
+ username: true,
265
+ },
266
+ },
267
+ late: {
268
+ select: {
269
+ id: true,
270
+ username: true,
271
+ },
272
+ },
273
+ absent: {
274
+ select: {
275
+ id: true,
276
+ username: true,
277
+ },
278
+ },
279
+ },
280
+ });
281
+
282
+ return attendance;
283
+ }),
284
+ });
@@ -0,0 +1,286 @@
1
+ import { z } from "zod";
2
+ import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
3
+ import { TRPCError } from "@trpc/server";
4
+ import { prisma } from "../lib/prisma";
5
+ import { v4 as uuidv4 } from 'uuid';
6
+ import { compare, hash } from "bcryptjs";
7
+ import { transport } from "../utils/email";
8
+
9
+ const loginSchema = z.object({
10
+ username: z.string(),
11
+ password: z.string(),
12
+ });
13
+
14
+ const registerSchema = z.object({
15
+ username: z.string().min(3, "Username must be at least 3 characters"),
16
+ email: z.string().email("Invalid email address"),
17
+ password: z.string().min(6, "Password must be at least 6 characters"),
18
+ confirmPassword: z.string(),
19
+ }).refine((data) => data.password === data.confirmPassword, {
20
+ message: "Passwords don't match",
21
+ path: ["confirmPassword"],
22
+ });
23
+
24
+ export const authRouter = createTRPCRouter({
25
+ register: publicProcedure
26
+ .input(registerSchema)
27
+ .mutation(async ({ input }) => {
28
+ const { username, email, password } = input;
29
+
30
+ // Check if username already exists
31
+ const existingUser = await prisma.user.findFirst({
32
+ where: {
33
+ OR: [
34
+ { username },
35
+ { email }
36
+ ]
37
+ },
38
+ select: {
39
+ id: true,
40
+ username: true,
41
+ email: true,
42
+ verified: true,
43
+ }
44
+ });
45
+
46
+ if (existingUser && existingUser.verified) {
47
+ if (existingUser.username === username) {
48
+ throw new TRPCError({
49
+ code: "CONFLICT",
50
+ message: "Username already exists",
51
+ });
52
+ }
53
+ if (existingUser.email === email) {
54
+ throw new TRPCError({
55
+ code: "CONFLICT",
56
+ message: "Email already exists",
57
+ });
58
+ }
59
+ } else if (existingUser && !existingUser.verified) {
60
+ await prisma.session.deleteMany({
61
+ where: { userId: existingUser.id },
62
+ });
63
+
64
+ await prisma.user.delete({
65
+ where: { id: existingUser.id },
66
+ });
67
+ }
68
+
69
+ // Create new user
70
+ const user = await prisma.user.create({
71
+ data: {
72
+ username,
73
+ email,
74
+ password: await hash(password, 10),
75
+ profile: {},
76
+ },
77
+ select: {
78
+ id: true,
79
+ username: true,
80
+ email: true,
81
+ }
82
+ });
83
+
84
+ const verificationToken = await prisma.session.create({
85
+ data: {
86
+ id: uuidv4(),
87
+ userId: user.id,
88
+ expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
89
+ },
90
+ });
91
+
92
+ await transport.sendMail({
93
+ from: 'noreply@studious.sh',
94
+ to: user.email,
95
+ subject: 'Verify your email',
96
+ text: `Click the link to verify your email: ${process.env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`,
97
+ });
98
+
99
+ return {
100
+ user: {
101
+ id: user.id,
102
+ username: user.username,
103
+ },
104
+ };
105
+ }),
106
+
107
+ login: publicProcedure
108
+ .input(loginSchema)
109
+ .mutation(async ({ input }) => {
110
+ const { username, password } = input;
111
+
112
+ const user = await prisma.user.findFirst({
113
+ where: { username },
114
+ select: {
115
+ id: true,
116
+ username: true,
117
+ password: true,
118
+ email: true,
119
+ verified: true,
120
+ }
121
+ });
122
+
123
+ if (!user) {
124
+ throw new TRPCError({
125
+ code: "UNAUTHORIZED",
126
+ message: "Invalid username or password",
127
+ });
128
+ }
129
+
130
+ if (await compare(password, user.password)) {
131
+ throw new TRPCError({
132
+ code: "UNAUTHORIZED",
133
+ message: "Invalid username or password",
134
+ });
135
+ }
136
+
137
+ if (!user.verified) {
138
+ return {
139
+ verified: false,
140
+ user: {
141
+ email: user.email,
142
+ },
143
+ }
144
+ }
145
+
146
+ // Create a new session
147
+ const session = await prisma.session.create({
148
+ data: {
149
+ id: uuidv4(),
150
+ userId: user.id,
151
+ expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
152
+ },
153
+ });
154
+
155
+ return {
156
+ token: session.id,
157
+ user: {
158
+ id: user.id,
159
+ username: user.username,
160
+ },
161
+ };
162
+ }),
163
+
164
+ logout: publicProcedure
165
+ .mutation(async ({ ctx }) => {
166
+ if (!ctx.user) {
167
+ throw new TRPCError({
168
+ code: "UNAUTHORIZED",
169
+ message: "Not authenticated",
170
+ });
171
+ }
172
+
173
+ // Delete the current session
174
+ await prisma.session.deleteMany({
175
+ where: { userId: ctx.user.id },
176
+ });
177
+
178
+ return { success: true };
179
+ }),
180
+
181
+ check: protectedProcedure
182
+ .query(async ({ ctx }) => {
183
+ if (!ctx.user) {
184
+ throw new TRPCError({
185
+ code: "UNAUTHORIZED",
186
+ message: "Not authenticated",
187
+ });
188
+ }
189
+
190
+ const user = await prisma.user.findUnique({
191
+ where: { id: ctx.user.id },
192
+ select: {
193
+ id: true,
194
+ username: true,
195
+ }
196
+ });
197
+
198
+ if (!user) {
199
+ throw new TRPCError({
200
+ code: "NOT_FOUND",
201
+ message: "User not found",
202
+ });
203
+ }
204
+
205
+ return {user};
206
+ }),
207
+ resendVerificationEmail: publicProcedure
208
+ .input(z.object({
209
+ email: z.string().email(),
210
+ }))
211
+ .mutation(async ({ input }) => {
212
+ const { email } = input;
213
+
214
+ const user = await prisma.user.findFirst({
215
+ where: {
216
+ email,
217
+ },
218
+ select: {
219
+ id: true,
220
+ email: true,
221
+ },
222
+ });
223
+
224
+ if (!user) {
225
+ throw new TRPCError({
226
+ code: "NOT_FOUND",
227
+ message: "User not found",
228
+ });
229
+ }
230
+
231
+ await prisma.session.deleteMany({
232
+ where: { userId: user?.id },
233
+ });
234
+
235
+ const verificationToken = await prisma.session.create({
236
+ data: {
237
+ id: uuidv4(),
238
+ userId: user.id,
239
+ expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30),
240
+ },
241
+ });
242
+
243
+ await transport.sendMail({
244
+ from: 'noreply@studious.sh',
245
+ to: user.email,
246
+ subject: 'Verify your email',
247
+ text: `Click the link to verify your email: ${process.env.NEXT_PUBLIC_APP_URL}/verify/${verificationToken.id}`,
248
+ });
249
+
250
+ return { success: true };
251
+ }),
252
+ verify: publicProcedure
253
+ .input(z.object({
254
+ token: z.string(),
255
+ }))
256
+ .mutation(async ({ input }) => {
257
+ const { token } = input;
258
+
259
+ const session = await prisma.session.findUnique({
260
+ where: { id: token },
261
+ });
262
+
263
+ if (!session) {
264
+ throw new TRPCError({
265
+ code: "NOT_FOUND",
266
+ message: "Session not found",
267
+ });
268
+ }
269
+
270
+ if (session.expiresAt && session.expiresAt < new Date()) {
271
+ throw new TRPCError({
272
+ code: "UNAUTHORIZED",
273
+ message: "Session expired",
274
+ });
275
+ }
276
+
277
+ await prisma.user.update({
278
+ where: { id: session.userId! },
279
+ data: {
280
+ verified: true,
281
+ },
282
+ });
283
+
284
+ return { success: true };
285
+ }),
286
+ });