@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.
- package/dist/index.js +4 -4
- package/dist/middleware/auth.js +1 -1
- package/dist/middleware/logging.js +1 -1
- package/dist/routers/_app.js +1 -1
- package/dist/routers/agenda.js +1 -1
- package/dist/routers/announcement.js +1 -1
- package/dist/routers/assignment.js +3 -3
- package/dist/routers/attendance.js +1 -1
- package/dist/routers/auth.js +2 -2
- package/dist/routers/class.js +2 -2
- package/dist/routers/event.js +1 -1
- package/dist/routers/file.js +2 -2
- package/dist/routers/section.js +1 -1
- package/dist/routers/user.js +2 -2
- package/dist/trpc.js +2 -2
- package/package.json +1 -6
- package/prisma/schema.prisma +228 -0
- package/src/exportType.ts +9 -0
- package/src/index.ts +94 -0
- package/src/lib/fileUpload.ts +163 -0
- package/src/lib/googleCloudStorage.ts +94 -0
- package/src/lib/prisma.ts +16 -0
- package/src/lib/thumbnailGenerator.ts +185 -0
- package/src/logger.ts +163 -0
- package/src/middleware/auth.ts +191 -0
- package/src/middleware/logging.ts +54 -0
- package/src/routers/_app.ts +34 -0
- package/src/routers/agenda.ts +79 -0
- package/src/routers/announcement.ts +134 -0
- package/src/routers/assignment.ts +1614 -0
- package/src/routers/attendance.ts +284 -0
- package/src/routers/auth.ts +286 -0
- package/src/routers/class.ts +753 -0
- package/src/routers/event.ts +509 -0
- package/src/routers/file.ts +96 -0
- package/src/routers/section.ts +138 -0
- package/src/routers/user.ts +82 -0
- package/src/socket/handlers.ts +143 -0
- package/src/trpc.ts +90 -0
- package/src/types/trpc.ts +15 -0
- package/src/utils/email.ts +11 -0
- package/src/utils/generateInviteCode.ts +8 -0
- package/src/utils/logger.ts +156 -0
- package/tsconfig.json +17 -0
- package/generated/prisma/client.d.ts +0 -1
- package/generated/prisma/client.js +0 -4
- package/generated/prisma/default.d.ts +0 -1
- package/generated/prisma/default.js +0 -4
- package/generated/prisma/edge.d.ts +0 -1
- package/generated/prisma/edge.js +0 -389
- package/generated/prisma/index-browser.js +0 -375
- package/generated/prisma/index.d.ts +0 -34865
- package/generated/prisma/index.js +0 -410
- package/generated/prisma/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/generated/prisma/package.json +0 -140
- package/generated/prisma/runtime/edge-esm.js +0 -34
- package/generated/prisma/runtime/edge.js +0 -34
- package/generated/prisma/runtime/index-browser.d.ts +0 -370
- package/generated/prisma/runtime/index-browser.js +0 -16
- package/generated/prisma/runtime/library.d.ts +0 -3647
- package/generated/prisma/runtime/library.js +0 -146
- package/generated/prisma/runtime/react-native.js +0 -83
- package/generated/prisma/runtime/wasm.js +0 -35
- package/generated/prisma/schema.prisma +0 -304
- package/generated/prisma/wasm.d.ts +0 -1
- package/generated/prisma/wasm.js +0 -375
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { TRPCError } from '@trpc/server';
|
|
2
|
+
import { prisma } from '../lib/prisma';
|
|
3
|
+
import type { MiddlewareContext } from '../types/trpc';
|
|
4
|
+
|
|
5
|
+
export const createAuthMiddleware = (t: any) => {
|
|
6
|
+
|
|
7
|
+
// Auth middleware
|
|
8
|
+
const isAuthed = t.middleware(async ({ next, ctx }: MiddlewareContext) => {
|
|
9
|
+
const startTime = Date.now();
|
|
10
|
+
// Get user from request headers
|
|
11
|
+
const userHeader = ctx.req.headers['x-user'];
|
|
12
|
+
|
|
13
|
+
if (!userHeader) {
|
|
14
|
+
throw new TRPCError({
|
|
15
|
+
code: 'UNAUTHORIZED',
|
|
16
|
+
message: 'Not authenticated - no token found',
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const token = typeof userHeader === 'string' ? userHeader : userHeader[0];
|
|
22
|
+
|
|
23
|
+
// Find user by session token
|
|
24
|
+
const user = await prisma.user.findFirst({
|
|
25
|
+
where: {
|
|
26
|
+
sessions: {
|
|
27
|
+
some: {
|
|
28
|
+
id: token
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
select: {
|
|
33
|
+
id: true,
|
|
34
|
+
username: true,
|
|
35
|
+
// institutionId: true,
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!user) {
|
|
40
|
+
throw new TRPCError({
|
|
41
|
+
code: 'UNAUTHORIZED',
|
|
42
|
+
message: 'Invalid or expired session',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(user)
|
|
47
|
+
|
|
48
|
+
return next({
|
|
49
|
+
ctx: {
|
|
50
|
+
...ctx,
|
|
51
|
+
user,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.log(error)
|
|
56
|
+
throw new TRPCError({
|
|
57
|
+
code: 'UNAUTHORIZED',
|
|
58
|
+
message: 'Invalid user data',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Add computed flags middleware
|
|
64
|
+
const addComputedFlags = t.middleware(async ({ next, ctx }: MiddlewareContext) => {
|
|
65
|
+
if (!ctx.user) {
|
|
66
|
+
throw new TRPCError({
|
|
67
|
+
code: 'UNAUTHORIZED',
|
|
68
|
+
message: 'Not authenticated',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get all classes where user is a teacher
|
|
73
|
+
const teacherClasses = await prisma.class.findMany({
|
|
74
|
+
where: {
|
|
75
|
+
teachers: {
|
|
76
|
+
some: {
|
|
77
|
+
id: ctx.user.id
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
select: {
|
|
82
|
+
id: true
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return next({
|
|
87
|
+
ctx: {
|
|
88
|
+
...ctx,
|
|
89
|
+
isTeacher: teacherClasses.length > 0,
|
|
90
|
+
teacherClassIds: teacherClasses.map((c: { id: string }) => c.id)
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Student middleware
|
|
96
|
+
const isMemberInClass = t.middleware(async ({ next, ctx, input }: MiddlewareContext) => {
|
|
97
|
+
if (!ctx.user) {
|
|
98
|
+
throw new TRPCError({
|
|
99
|
+
code: 'UNAUTHORIZED',
|
|
100
|
+
message: 'Not authenticated',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const classId = (input as { classId: string })?.classId;
|
|
105
|
+
|
|
106
|
+
if (!classId) {
|
|
107
|
+
throw new TRPCError({
|
|
108
|
+
code: 'BAD_REQUEST',
|
|
109
|
+
message: 'classId is required',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const isMember = await prisma.class.findFirst({
|
|
114
|
+
where: {
|
|
115
|
+
id: classId,
|
|
116
|
+
OR: [
|
|
117
|
+
{
|
|
118
|
+
students: {
|
|
119
|
+
some: {
|
|
120
|
+
id: ctx.user.id
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
teachers: {
|
|
126
|
+
some: {
|
|
127
|
+
id: ctx.user.id
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!isMember) {
|
|
136
|
+
throw new TRPCError({
|
|
137
|
+
code: 'FORBIDDEN',
|
|
138
|
+
message: 'Not a member in this class',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return next();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Teacher middleware
|
|
146
|
+
const isTeacherInClass = t.middleware(async ({ next, ctx, input }: MiddlewareContext) => {
|
|
147
|
+
if (!ctx.user) {
|
|
148
|
+
throw new TRPCError({
|
|
149
|
+
code: 'UNAUTHORIZED',
|
|
150
|
+
message: 'Not authenticated',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const classId = input.classId;
|
|
155
|
+
if (!classId) {
|
|
156
|
+
throw new TRPCError({
|
|
157
|
+
code: 'BAD_REQUEST',
|
|
158
|
+
message: 'classId is required',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const isTeacher = await prisma.class.findFirst({
|
|
163
|
+
where: {
|
|
164
|
+
id: classId,
|
|
165
|
+
teachers: {
|
|
166
|
+
some: {
|
|
167
|
+
id: ctx.user.id
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if (!isTeacher) {
|
|
176
|
+
throw new TRPCError({
|
|
177
|
+
code: 'FORBIDDEN',
|
|
178
|
+
message: 'Not a teacher in this class',
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return next();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
isAuthed,
|
|
187
|
+
addComputedFlags,
|
|
188
|
+
isMemberInClass,
|
|
189
|
+
isTeacherInClass,
|
|
190
|
+
};
|
|
191
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { MiddlewareContext } from '../types/trpc';
|
|
2
|
+
import { logger } from '../utils/logger';
|
|
3
|
+
|
|
4
|
+
export const createLoggingMiddleware = (t: any) => {
|
|
5
|
+
return t.middleware(async ({ path, type, next, ctx }: MiddlewareContext) => {
|
|
6
|
+
const start = Date.now();
|
|
7
|
+
const requestId = crypto.randomUUID();
|
|
8
|
+
|
|
9
|
+
// Log request
|
|
10
|
+
logger.info('tRPC Request', {
|
|
11
|
+
requestId,
|
|
12
|
+
path,
|
|
13
|
+
type,
|
|
14
|
+
// input,
|
|
15
|
+
timestamp: new Date().toISOString(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const result = await next();
|
|
20
|
+
const durationMs = Date.now() - start;
|
|
21
|
+
|
|
22
|
+
// Log successful response
|
|
23
|
+
logger.info('tRPC Response', {
|
|
24
|
+
requestId,
|
|
25
|
+
path,
|
|
26
|
+
type,
|
|
27
|
+
durationMs,
|
|
28
|
+
ok: result.ok,
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const durationMs = Date.now() - start;
|
|
35
|
+
|
|
36
|
+
// Log error response
|
|
37
|
+
logger.error('tRPC Error' + path, {
|
|
38
|
+
requestId,
|
|
39
|
+
path,
|
|
40
|
+
type,
|
|
41
|
+
durationMs,
|
|
42
|
+
error: error instanceof Error ? {
|
|
43
|
+
path,
|
|
44
|
+
name: error.name,
|
|
45
|
+
message: error.message,
|
|
46
|
+
stack: error.stack,
|
|
47
|
+
} : error,
|
|
48
|
+
timestamp: new Date().toISOString(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createTRPCRouter } from "../trpc";
|
|
2
|
+
import { classRouter } from "./class";
|
|
3
|
+
import { announcementRouter } from "./announcement";
|
|
4
|
+
import { assignmentRouter } from "./assignment";
|
|
5
|
+
import { userRouter } from "./user";
|
|
6
|
+
import { createCallerFactory } from "../trpc";
|
|
7
|
+
import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
|
|
8
|
+
import { sectionRouter } from "./section";
|
|
9
|
+
import { attendanceRouter } from "./attendance";
|
|
10
|
+
import { eventRouter } from "./event";
|
|
11
|
+
import { authRouter } from "./auth";
|
|
12
|
+
import { agendaRouter } from "./agenda";
|
|
13
|
+
import { fileRouter } from "./file";
|
|
14
|
+
|
|
15
|
+
export const appRouter = createTRPCRouter({
|
|
16
|
+
class: classRouter,
|
|
17
|
+
announcement: announcementRouter,
|
|
18
|
+
assignment: assignmentRouter,
|
|
19
|
+
user: userRouter,
|
|
20
|
+
section: sectionRouter,
|
|
21
|
+
attendance: attendanceRouter,
|
|
22
|
+
event: eventRouter,
|
|
23
|
+
auth: authRouter,
|
|
24
|
+
agenda: agendaRouter,
|
|
25
|
+
file: fileRouter,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Export type router type definition
|
|
29
|
+
export type AppRouter = typeof appRouter;
|
|
30
|
+
export type RouterInputs = inferRouterInputs<AppRouter>;
|
|
31
|
+
export type RouterOutputs = inferRouterOutputs<AppRouter>;
|
|
32
|
+
|
|
33
|
+
// Export caller
|
|
34
|
+
export const createCaller = createCallerFactory(appRouter);
|
|
@@ -0,0 +1,79 @@
|
|
|
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 { addDays, startOfDay, endOfDay } from "date-fns";
|
|
6
|
+
|
|
7
|
+
export const agendaRouter = createTRPCRouter({
|
|
8
|
+
get: protectedProcedure
|
|
9
|
+
.input(z.object({
|
|
10
|
+
weekStart: z.string(),
|
|
11
|
+
}))
|
|
12
|
+
.query(async ({ ctx, input }) => {
|
|
13
|
+
if (!ctx.user) {
|
|
14
|
+
throw new TRPCError({
|
|
15
|
+
code: "UNAUTHORIZED",
|
|
16
|
+
message: "You must be logged in to get your agenda",
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const weekStart = startOfDay(new Date(input.weekStart));
|
|
21
|
+
const weekEnd = endOfDay(addDays(weekStart, 6));
|
|
22
|
+
|
|
23
|
+
const [personalEvents, classEvents] = await Promise.all([
|
|
24
|
+
// Get personal events
|
|
25
|
+
prisma.event.findMany({
|
|
26
|
+
where: {
|
|
27
|
+
userId: ctx.user.id,
|
|
28
|
+
startTime: {
|
|
29
|
+
gte: weekStart,
|
|
30
|
+
lte: weekEnd,
|
|
31
|
+
},
|
|
32
|
+
class: {
|
|
33
|
+
is: null,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
include: {
|
|
37
|
+
class: true,
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
// Get class events
|
|
41
|
+
prisma.event.findMany({
|
|
42
|
+
where: {
|
|
43
|
+
class: {
|
|
44
|
+
OR: [
|
|
45
|
+
{
|
|
46
|
+
teachers: {
|
|
47
|
+
some: {
|
|
48
|
+
id: ctx.user.id,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
students: {
|
|
54
|
+
some: {
|
|
55
|
+
id: ctx.user.id,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
startTime: {
|
|
62
|
+
gte: weekStart,
|
|
63
|
+
lte: weekEnd,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
include: {
|
|
67
|
+
class: true,
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
events: {
|
|
74
|
+
personal: personalEvents,
|
|
75
|
+
class: classEvents,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createTRPCRouter, protectedClassMemberProcedure, protectedTeacherProcedure, protectedProcedure } from "../trpc";
|
|
3
|
+
import { prisma } from "../lib/prisma";
|
|
4
|
+
import { TRPCError } from "@trpc/server";
|
|
5
|
+
|
|
6
|
+
const AnnouncementSelect = {
|
|
7
|
+
id: true,
|
|
8
|
+
teacher: {
|
|
9
|
+
select: {
|
|
10
|
+
id: true,
|
|
11
|
+
username: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
remarks: true,
|
|
15
|
+
createdAt: true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const announcementRouter = createTRPCRouter({
|
|
19
|
+
getAll: protectedClassMemberProcedure
|
|
20
|
+
.input(z.object({
|
|
21
|
+
classId: z.string(),
|
|
22
|
+
}))
|
|
23
|
+
.query(async ({ ctx, input }) => {
|
|
24
|
+
const announcements = await prisma.announcement.findMany({
|
|
25
|
+
where: {
|
|
26
|
+
classId: input.classId,
|
|
27
|
+
},
|
|
28
|
+
select: AnnouncementSelect,
|
|
29
|
+
orderBy: {
|
|
30
|
+
createdAt: 'desc',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
announcements,
|
|
36
|
+
};
|
|
37
|
+
}),
|
|
38
|
+
|
|
39
|
+
create: protectedTeacherProcedure
|
|
40
|
+
.input(z.object({
|
|
41
|
+
classId: z.string(),
|
|
42
|
+
remarks: z.string(),
|
|
43
|
+
}))
|
|
44
|
+
.mutation(async ({ ctx, input }) => {
|
|
45
|
+
const announcement = await prisma.announcement.create({
|
|
46
|
+
data: {
|
|
47
|
+
remarks: input.remarks,
|
|
48
|
+
teacher: {
|
|
49
|
+
connect: {
|
|
50
|
+
id: ctx.user?.id,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
class: {
|
|
54
|
+
connect: {
|
|
55
|
+
id: input.classId,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
select: AnnouncementSelect,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
announcement,
|
|
64
|
+
};
|
|
65
|
+
}),
|
|
66
|
+
|
|
67
|
+
update: protectedProcedure
|
|
68
|
+
.input(z.object({
|
|
69
|
+
id: z.string(),
|
|
70
|
+
data: z.object({
|
|
71
|
+
content: z.string(),
|
|
72
|
+
}),
|
|
73
|
+
}))
|
|
74
|
+
.mutation(async ({ ctx, input }) => {
|
|
75
|
+
|
|
76
|
+
const announcement = await prisma.announcement.findUnique({
|
|
77
|
+
where: { id: input.id },
|
|
78
|
+
include: {
|
|
79
|
+
class: {
|
|
80
|
+
include: {
|
|
81
|
+
teachers: true,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!announcement) {
|
|
88
|
+
throw new TRPCError({
|
|
89
|
+
code: "NOT_FOUND",
|
|
90
|
+
message: "Announcement not found",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const updatedAnnouncement = await prisma.announcement.update({
|
|
95
|
+
where: { id: input.id },
|
|
96
|
+
data: {
|
|
97
|
+
remarks: input.data.content,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return { announcement: updatedAnnouncement };
|
|
102
|
+
}),
|
|
103
|
+
|
|
104
|
+
delete: protectedProcedure
|
|
105
|
+
.input(z.object({
|
|
106
|
+
id: z.string(),
|
|
107
|
+
}))
|
|
108
|
+
.mutation(async ({ ctx, input }) => {
|
|
109
|
+
|
|
110
|
+
const announcement = await prisma.announcement.findUnique({
|
|
111
|
+
where: { id: input.id },
|
|
112
|
+
include: {
|
|
113
|
+
class: {
|
|
114
|
+
include: {
|
|
115
|
+
teachers: true,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (!announcement) {
|
|
122
|
+
throw new TRPCError({
|
|
123
|
+
code: "NOT_FOUND",
|
|
124
|
+
message: "Announcement not found",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await prisma.announcement.delete({
|
|
129
|
+
where: { id: input.id },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return { success: true };
|
|
133
|
+
}),
|
|
134
|
+
});
|