@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,138 @@
|
|
|
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 createSectionSchema = z.object({
|
|
7
|
+
classId: z.string(),
|
|
8
|
+
name: z.string(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const updateSectionSchema = z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
classId: z.string(),
|
|
14
|
+
name: z.string(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const deleteSectionSchema = z.object({
|
|
18
|
+
id: z.string(),
|
|
19
|
+
classId: z.string(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const sectionRouter = createTRPCRouter({
|
|
23
|
+
create: protectedProcedure
|
|
24
|
+
.input(createSectionSchema)
|
|
25
|
+
.mutation(async ({ ctx, input }) => {
|
|
26
|
+
if (!ctx.user) {
|
|
27
|
+
throw new TRPCError({
|
|
28
|
+
code: "UNAUTHORIZED",
|
|
29
|
+
message: "User must be authenticated",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Verify user is a teacher of the class
|
|
34
|
+
const classData = await prisma.class.findFirst({
|
|
35
|
+
where: {
|
|
36
|
+
id: input.classId,
|
|
37
|
+
teachers: {
|
|
38
|
+
some: {
|
|
39
|
+
id: ctx.user.id,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!classData) {
|
|
46
|
+
throw new TRPCError({
|
|
47
|
+
code: "NOT_FOUND",
|
|
48
|
+
message: "Class not found or you are not a teacher",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const section = await prisma.section.create({
|
|
53
|
+
data: {
|
|
54
|
+
name: input.name,
|
|
55
|
+
class: {
|
|
56
|
+
connect: { id: input.classId },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return section;
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
update: protectedProcedure
|
|
65
|
+
.input(updateSectionSchema)
|
|
66
|
+
.mutation(async ({ ctx, input }) => {
|
|
67
|
+
if (!ctx.user) {
|
|
68
|
+
throw new TRPCError({
|
|
69
|
+
code: "UNAUTHORIZED",
|
|
70
|
+
message: "User must be authenticated",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Verify user is a teacher of the class
|
|
75
|
+
const classData = await prisma.class.findFirst({
|
|
76
|
+
where: {
|
|
77
|
+
id: input.classId,
|
|
78
|
+
teachers: {
|
|
79
|
+
some: {
|
|
80
|
+
id: ctx.user.id,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!classData) {
|
|
87
|
+
throw new TRPCError({
|
|
88
|
+
code: "NOT_FOUND",
|
|
89
|
+
message: "Class not found or you are not a teacher",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const section = await prisma.section.update({
|
|
94
|
+
where: { id: input.id },
|
|
95
|
+
data: {
|
|
96
|
+
name: input.name,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return section;
|
|
101
|
+
}),
|
|
102
|
+
|
|
103
|
+
delete: protectedProcedure
|
|
104
|
+
.input(deleteSectionSchema)
|
|
105
|
+
.mutation(async ({ ctx, input }) => {
|
|
106
|
+
if (!ctx.user) {
|
|
107
|
+
throw new TRPCError({
|
|
108
|
+
code: "UNAUTHORIZED",
|
|
109
|
+
message: "User must be authenticated",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Verify user is a teacher of the class
|
|
114
|
+
const classData = await prisma.class.findFirst({
|
|
115
|
+
where: {
|
|
116
|
+
id: input.classId,
|
|
117
|
+
teachers: {
|
|
118
|
+
some: {
|
|
119
|
+
id: ctx.user.id,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (!classData) {
|
|
126
|
+
throw new TRPCError({
|
|
127
|
+
code: "NOT_FOUND",
|
|
128
|
+
message: "Class not found or you are not a teacher",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await prisma.section.delete({
|
|
133
|
+
where: { id: input.id },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return { id: input.id };
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
|
3
|
+
import { TRPCError } from "@trpc/server";
|
|
4
|
+
import { prisma } from "../lib/prisma";
|
|
5
|
+
import { uploadFiles, type UploadedFile } from "../lib/fileUpload";
|
|
6
|
+
|
|
7
|
+
const fileSchema = z.object({
|
|
8
|
+
name: z.string(),
|
|
9
|
+
type: z.string(),
|
|
10
|
+
size: z.number(),
|
|
11
|
+
data: z.string(), // base64 encoded file data
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const updateProfileSchema = z.object({
|
|
15
|
+
profile: z.record(z.any()),
|
|
16
|
+
profilePicture: fileSchema.optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const userRouter = createTRPCRouter({
|
|
20
|
+
getProfile: protectedProcedure
|
|
21
|
+
.query(async ({ ctx }) => {
|
|
22
|
+
if (!ctx.user) {
|
|
23
|
+
throw new TRPCError({
|
|
24
|
+
code: "UNAUTHORIZED",
|
|
25
|
+
message: "User must be authenticated",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const user = await prisma.user.findUnique({
|
|
30
|
+
where: { id: ctx.user.id },
|
|
31
|
+
select: {
|
|
32
|
+
id: true,
|
|
33
|
+
username: true,
|
|
34
|
+
profile: true,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!user) {
|
|
39
|
+
throw new TRPCError({
|
|
40
|
+
code: "NOT_FOUND",
|
|
41
|
+
message: "User not found",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return user;
|
|
46
|
+
}),
|
|
47
|
+
|
|
48
|
+
updateProfile: protectedProcedure
|
|
49
|
+
.input(updateProfileSchema)
|
|
50
|
+
.mutation(async ({ ctx, input }) => {
|
|
51
|
+
if (!ctx.user) {
|
|
52
|
+
throw new TRPCError({
|
|
53
|
+
code: "UNAUTHORIZED",
|
|
54
|
+
message: "User must be authenticated",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let uploadedFiles: UploadedFile[] = [];
|
|
59
|
+
if (input.profilePicture) {
|
|
60
|
+
// Store profile picture in a user-specific directory
|
|
61
|
+
uploadedFiles = await uploadFiles([input.profilePicture], ctx.user.id, `users/${ctx.user.id}/profile`);
|
|
62
|
+
|
|
63
|
+
// Add profile picture path to profile data
|
|
64
|
+
input.profile.profilePicture = uploadedFiles[0].path;
|
|
65
|
+
input.profile.profilePictureThumbnail = uploadedFiles[0].thumbnailId;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const updatedUser = await prisma.user.update({
|
|
69
|
+
where: { id: ctx.user.id },
|
|
70
|
+
data: {
|
|
71
|
+
profile: input.profile,
|
|
72
|
+
},
|
|
73
|
+
select: {
|
|
74
|
+
id: true,
|
|
75
|
+
username: true,
|
|
76
|
+
profile: true,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return updatedUser;
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Socket, Server } from 'socket.io';
|
|
2
|
+
import { logger } from '../utils/logger';
|
|
3
|
+
|
|
4
|
+
export const setupSocketHandlers = (io: Server) => {
|
|
5
|
+
io.on('connection', (socket: Socket) => {
|
|
6
|
+
logger.info('Client connected', { socketId: socket.id });
|
|
7
|
+
|
|
8
|
+
socket.on('disconnect', (reason) => {
|
|
9
|
+
logger.info('Client disconnected', { socketId: socket.id, reason });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
socket.on('error', (error) => {
|
|
13
|
+
logger.error('Socket error', { socketId: socket.id, error });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Class Room Management
|
|
17
|
+
socket.on('create-class', (data: { class: any }) => {
|
|
18
|
+
io.emit('class-created', data.class);
|
|
19
|
+
logger.info('Class created', { class: data.class });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
socket.on('delete-class', (data: { classId: string }) => {
|
|
23
|
+
io.emit('class-deleted', data.classId);
|
|
24
|
+
logger.info('Class deleted', { classId: data.classId });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
socket.on('update-class', (data: { class: any }) => {
|
|
28
|
+
io.emit('class-updated', data.class);
|
|
29
|
+
logger.info('Class updated', { class: data.class });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
socket.on('join-class', (classId: string, callback) => {
|
|
33
|
+
try {
|
|
34
|
+
socket.join(`class-${classId}`);
|
|
35
|
+
logger.info('Client joined class room', { socketId: socket.id, classId });
|
|
36
|
+
|
|
37
|
+
if (callback) {
|
|
38
|
+
callback(classId);
|
|
39
|
+
} else {
|
|
40
|
+
socket.emit('joined-class', classId);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logger.error('Error joining class room', { socketId: socket.id, classId, error });
|
|
44
|
+
if (callback) {
|
|
45
|
+
callback(null);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Assignment Events
|
|
51
|
+
socket.on('assignment-create', (data: { classId: string, assignment: any }) => {
|
|
52
|
+
if (data.classId && data.assignment) {
|
|
53
|
+
io.in(`class-${data.classId}`).emit('assignment-created', data.assignment);
|
|
54
|
+
logger.info('Assignment created', { classId: data.classId, assignmentId: data.assignment.id });
|
|
55
|
+
} else {
|
|
56
|
+
logger.error('Invalid assignment data format', { data });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
socket.on('assignment-update', (data: { classId: string, assignment: any }) => {
|
|
61
|
+
io.in(`class-${data.classId}`).emit('assignment-updated', data.assignment);
|
|
62
|
+
logger.info('Assignment updated', { classId: data.classId, assignmentId: data.assignment.id });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
socket.on('assignment-delete', (data: { classId: string, assignmentId: string }) => {
|
|
66
|
+
io.in(`class-${data.classId}`).emit('assignment-deleted', data.assignmentId);
|
|
67
|
+
logger.info('Assignment deleted', { classId: data.classId, assignmentId: data.assignmentId });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Submission Events
|
|
71
|
+
socket.on('submission-update', (data: { classId: string, submission: any }) => {
|
|
72
|
+
io.in(`class-${data.classId}`).emit('submission-updated', data.submission);
|
|
73
|
+
logger.info('Submission updated', { classId: data.classId, submissionId: data.submission.id });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Announcement Events
|
|
77
|
+
socket.on('new-announcement', (data: { classId: string, announcement: any }) => {
|
|
78
|
+
io.in(`class-${data.classId}`).emit('announcement-created', data.announcement);
|
|
79
|
+
logger.info('New announcement created', { classId: data.classId, announcementId: data.announcement.id });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Section Events
|
|
83
|
+
socket.on('section-create', (data: { classId: string, section: any }) => {
|
|
84
|
+
io.in(`class-${data.classId}`).emit('section-created', data.section);
|
|
85
|
+
logger.info('Section created', { classId: data.classId, sectionId: data.section.id });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
socket.on('section-update', (data: { classId: string, section: any }) => {
|
|
89
|
+
io.in(`class-${data.classId}`).emit('section-updated', data.section);
|
|
90
|
+
logger.info('Section updated', { classId: data.classId, sectionId: data.section.id });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
socket.on('section-delete', (data: { classId: string, sectionId: string }) => {
|
|
94
|
+
io.in(`class-${data.classId}`).emit('section-deleted', data.sectionId);
|
|
95
|
+
logger.info('Section deleted', { classId: data.classId, sectionId: data.sectionId });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Member Events
|
|
99
|
+
socket.on('member-update', (data: { classId: string, member: any }) => {
|
|
100
|
+
io.in(`class-${data.classId}`).emit('member-updated', data.member);
|
|
101
|
+
logger.info('Member updated', { classId: data.classId, memberId: data.member.id });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
socket.on('member-delete', (data: { classId: string, memberId: string }) => {
|
|
105
|
+
io.in(`class-${data.classId}`).emit('member-deleted', data.memberId);
|
|
106
|
+
logger.info('Member deleted', { classId: data.classId, memberId: data.memberId });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Attendance Events
|
|
110
|
+
socket.on('attendance-update', (data: { classId: string, attendance: any }) => {
|
|
111
|
+
io.in(`class-${data.classId}`).emit('attendance-updated', data.attendance);
|
|
112
|
+
logger.info('Attendance updated', { classId: data.classId, attendanceId: data.attendance.id });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Event Events
|
|
116
|
+
socket.on('event-create', (data: { classId: string, event: any }) => {
|
|
117
|
+
if (data.classId) {
|
|
118
|
+
io.in(`class-${data.classId}`).emit('event-created', data.event);
|
|
119
|
+
} else {
|
|
120
|
+
io.emit('event-created', data.event);
|
|
121
|
+
}
|
|
122
|
+
logger.info('Event created', { classId: data.classId, eventId: data.event.id });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
socket.on('event-update', (data: { classId: string, event: any }) => {
|
|
126
|
+
if (data.classId) {
|
|
127
|
+
io.in(`class-${data.classId}`).emit('event-updated', data.event);
|
|
128
|
+
} else {
|
|
129
|
+
io.emit('event-updated', data.event);
|
|
130
|
+
}
|
|
131
|
+
logger.info('Event updated', { classId: data.classId, eventId: data.event.id });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
socket.on('event-delete', (data: { classId: string, eventId: string }) => {
|
|
135
|
+
if (data.classId) {
|
|
136
|
+
io.in(`class-${data.classId}`).emit('event-deleted', data.eventId);
|
|
137
|
+
} else {
|
|
138
|
+
io.emit('event-deleted', data.eventId);
|
|
139
|
+
}
|
|
140
|
+
logger.info('Event deleted', { classId: data.classId, eventId: data.eventId });
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
};
|
package/src/trpc.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { initTRPC, TRPCError } from '@trpc/server';
|
|
2
|
+
import { ZodError } from 'zod';
|
|
3
|
+
import { logger } from './utils/logger';
|
|
4
|
+
import { prisma } from './lib/prisma';
|
|
5
|
+
import { createLoggingMiddleware } from './middleware/logging';
|
|
6
|
+
import { createAuthMiddleware } from './middleware/auth';
|
|
7
|
+
import { Request, Response } from 'express';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
interface CreateContextOptions {
|
|
11
|
+
req: Request;
|
|
12
|
+
res: Response;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type Context = {
|
|
16
|
+
req: Request;
|
|
17
|
+
res: Response;
|
|
18
|
+
user: { id: string } | null;
|
|
19
|
+
meta?: {
|
|
20
|
+
classId?: string;
|
|
21
|
+
institutionId?: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const createTRPCContext = async (opts: CreateContextOptions): Promise<Context> => {
|
|
26
|
+
const { req, res } = opts;
|
|
27
|
+
|
|
28
|
+
// Get user from session/token
|
|
29
|
+
const token = req.headers.authorization?.split(' ')[1];
|
|
30
|
+
const user = token ? await prisma.user.findFirst({
|
|
31
|
+
where: {
|
|
32
|
+
sessions: {
|
|
33
|
+
some: {
|
|
34
|
+
id: token
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
select: {
|
|
39
|
+
id: true,
|
|
40
|
+
}
|
|
41
|
+
}) : null;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
req,
|
|
45
|
+
res,
|
|
46
|
+
user,
|
|
47
|
+
meta: {},
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const t = initTRPC.context<Context>().create({
|
|
52
|
+
errorFormatter({ shape, error }) {
|
|
53
|
+
logger.error('tRPC Error', {
|
|
54
|
+
code: shape.code,
|
|
55
|
+
message: error.message,
|
|
56
|
+
cause: error.cause,
|
|
57
|
+
stack: error.stack,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
...shape,
|
|
62
|
+
data: {
|
|
63
|
+
...shape.data,
|
|
64
|
+
zodError:
|
|
65
|
+
error.cause instanceof ZodError ? error.cause.flatten() : null,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Create middleware
|
|
72
|
+
const loggingMiddleware = createLoggingMiddleware(t);
|
|
73
|
+
const { isAuthed, isMemberInClass, isTeacherInClass } = createAuthMiddleware(t);
|
|
74
|
+
|
|
75
|
+
// Base procedures
|
|
76
|
+
export const createTRPCRouter = t.router;
|
|
77
|
+
export const publicProcedure = t.procedure.use(loggingMiddleware);
|
|
78
|
+
|
|
79
|
+
// Protected procedures
|
|
80
|
+
export const protectedProcedure = publicProcedure.use(isAuthed);
|
|
81
|
+
export const protectedClassMemberProcedure = protectedProcedure
|
|
82
|
+
.input(z.object({ classId: z.string() }).passthrough())
|
|
83
|
+
.use(isMemberInClass);
|
|
84
|
+
export const protectedTeacherProcedure = protectedProcedure
|
|
85
|
+
.input(z.object({ classId: z.string() }).passthrough())
|
|
86
|
+
.use(isTeacherInClass);
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
// Create caller factory
|
|
90
|
+
export const createCallerFactory = t.createCallerFactory;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { inferAsyncReturnType } from '@trpc/server';
|
|
2
|
+
import { createTRPCContext } from '../trpc';
|
|
3
|
+
|
|
4
|
+
export type Context = inferAsyncReturnType<typeof createTRPCContext> & {
|
|
5
|
+
isTeacher?: boolean;
|
|
6
|
+
teacherClassIds?: string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export interface MiddlewareContext {
|
|
10
|
+
ctx: Context;
|
|
11
|
+
next: (opts?: { ctx: Partial<Context> }) => Promise<any>;
|
|
12
|
+
input?: any;
|
|
13
|
+
path?: string;
|
|
14
|
+
type?: string;
|
|
15
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export enum LogLevel {
|
|
2
|
+
INFO = 'info',
|
|
3
|
+
WARN = 'warn',
|
|
4
|
+
ERROR = 'error',
|
|
5
|
+
DEBUG = 'debug'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type LogMode = 'silent' | 'minimal' | 'normal' | 'verbose';
|
|
9
|
+
|
|
10
|
+
interface LogMessage {
|
|
11
|
+
level: LogLevel;
|
|
12
|
+
message: string;
|
|
13
|
+
timestamp: string;
|
|
14
|
+
context?: Record<string, any>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ANSI color codes
|
|
18
|
+
const colors = {
|
|
19
|
+
reset: '\x1b[0m',
|
|
20
|
+
bright: '\x1b[1m',
|
|
21
|
+
dim: '\x1b[2m',
|
|
22
|
+
red: '\x1b[31m',
|
|
23
|
+
green: '\x1b[32m',
|
|
24
|
+
yellow: '\x1b[33m',
|
|
25
|
+
blue: '\x1b[34m',
|
|
26
|
+
magenta: '\x1b[35m',
|
|
27
|
+
cyan: '\x1b[36m',
|
|
28
|
+
white: '\x1b[37m',
|
|
29
|
+
gray: '\x1b[90m'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
class Logger {
|
|
33
|
+
private static instance: Logger;
|
|
34
|
+
private isDevelopment: boolean;
|
|
35
|
+
private mode: LogMode;
|
|
36
|
+
private levelColors: Record<LogLevel, string>;
|
|
37
|
+
private levelEmojis: Record<LogLevel, string>;
|
|
38
|
+
|
|
39
|
+
private constructor() {
|
|
40
|
+
this.isDevelopment = process.env.NODE_ENV === 'development';
|
|
41
|
+
|
|
42
|
+
this.mode = (process.env.LOG_MODE as LogMode) || 'normal';
|
|
43
|
+
|
|
44
|
+
this.levelColors = {
|
|
45
|
+
[LogLevel.INFO]: colors.blue,
|
|
46
|
+
[LogLevel.WARN]: colors.yellow,
|
|
47
|
+
[LogLevel.ERROR]: colors.red,
|
|
48
|
+
[LogLevel.DEBUG]: colors.magenta
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
this.levelEmojis = {
|
|
52
|
+
[LogLevel.INFO]: 'ℹ️',
|
|
53
|
+
[LogLevel.WARN]: '⚠️',
|
|
54
|
+
[LogLevel.ERROR]: '❌',
|
|
55
|
+
[LogLevel.DEBUG]: '🔍'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public static getInstance(): Logger {
|
|
60
|
+
if (!Logger.instance) {
|
|
61
|
+
Logger.instance = new Logger();
|
|
62
|
+
}
|
|
63
|
+
return Logger.instance;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public setMode(mode: LogMode) {
|
|
67
|
+
this.mode = mode;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private shouldLog(level: LogLevel): boolean {
|
|
71
|
+
const silent = [
|
|
72
|
+
LogLevel.ERROR,
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const minimal = [
|
|
76
|
+
LogLevel.ERROR,
|
|
77
|
+
LogLevel.WARN,
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const normal = [
|
|
81
|
+
LogLevel.ERROR,
|
|
82
|
+
LogLevel.WARN,
|
|
83
|
+
LogLevel.INFO,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
if (this.mode === 'silent') return silent.includes(level);
|
|
87
|
+
if (this.mode === 'minimal') return minimal.includes(level);
|
|
88
|
+
if (this.mode === 'normal') return normal.includes(level);
|
|
89
|
+
return true; // verbose mode
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private formatMessage(logMessage: LogMessage): string {
|
|
93
|
+
const { level, message, timestamp, context } = logMessage;
|
|
94
|
+
const color = this.levelColors[level];
|
|
95
|
+
const emoji = this.levelEmojis[level];
|
|
96
|
+
|
|
97
|
+
const timestampStr = colors.gray + `[${timestamp}]` + colors.reset;
|
|
98
|
+
const levelStr = color + `[${level.toUpperCase()}]` + colors.reset;
|
|
99
|
+
const emojiStr = emoji + ' ';
|
|
100
|
+
const messageStr = colors.bright + message + colors.reset;
|
|
101
|
+
|
|
102
|
+
const contextStr = context
|
|
103
|
+
? '\n' + colors.dim + 'Context: ' + JSON.stringify(context, null, 2) + colors.reset
|
|
104
|
+
: '';
|
|
105
|
+
|
|
106
|
+
return `${timestampStr} ${levelStr} ${emojiStr}${messageStr}${contextStr}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private log(level: LogLevel, message: string, context?: Record<string, any>) {
|
|
110
|
+
// if (this.shouldLog(level))
|
|
111
|
+
// return;
|
|
112
|
+
|
|
113
|
+
const logMessage: LogMessage = {
|
|
114
|
+
level,
|
|
115
|
+
message,
|
|
116
|
+
timestamp: new Date().toISOString(),
|
|
117
|
+
context
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const formattedMessage = this.formatMessage(logMessage);
|
|
121
|
+
|
|
122
|
+
switch (level) {
|
|
123
|
+
case LogLevel.ERROR:
|
|
124
|
+
console.error(formattedMessage);
|
|
125
|
+
break;
|
|
126
|
+
case LogLevel.WARN:
|
|
127
|
+
console.warn(formattedMessage);
|
|
128
|
+
break;
|
|
129
|
+
case LogLevel.DEBUG:
|
|
130
|
+
if (this.isDevelopment) {
|
|
131
|
+
console.debug(formattedMessage);
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
default:
|
|
135
|
+
console.log(formattedMessage);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public info(message: string, context?: Record<string, any>) {
|
|
140
|
+
this.log(LogLevel.INFO, message, context);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public warn(message: string, context?: Record<string, any>) {
|
|
144
|
+
this.log(LogLevel.WARN, message, context);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public error(message: string, context?: Record<string, any>) {
|
|
148
|
+
this.log(LogLevel.ERROR, message, context);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public debug(message: string, context?: Record<string, any>) {
|
|
152
|
+
this.log(LogLevel.DEBUG, message, context);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export const logger = Logger.getInstance();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"baseUrl": ".",
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./index"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./index"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./default"
|