@studious-lms/server 1.0.2 → 1.0.4
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/routers/_app.d.ts +216 -88
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/agenda.d.ts +5 -4
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts +8 -4
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +20 -4
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/attendance.d.ts +6 -4
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/auth.d.ts +10 -4
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/class.d.ts +26 -4
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/event.d.ts +11 -4
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/file.d.ts +5 -4
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/section.d.ts +7 -4
- package/dist/routers/section.d.ts.map +1 -1
- package/dist/routers/user.d.ts +6 -4
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/trpc.d.ts +43 -90
- package/dist/trpc.d.ts.map +1 -1
- package/package.json +2 -7
- 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
package/src/logger.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
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
|
+
// Environment detection
|
|
18
|
+
const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
|
|
19
|
+
const isBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
20
|
+
|
|
21
|
+
// ANSI color codes for Node.js
|
|
22
|
+
const ansiColors = {
|
|
23
|
+
reset: '\x1b[0m',
|
|
24
|
+
bright: '\x1b[1m',
|
|
25
|
+
dim: '\x1b[2m',
|
|
26
|
+
red: '\x1b[31m',
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
yellow: '\x1b[33m',
|
|
29
|
+
blue: '\x1b[34m',
|
|
30
|
+
magenta: '\x1b[35m',
|
|
31
|
+
cyan: '\x1b[36m',
|
|
32
|
+
white: '\x1b[37m',
|
|
33
|
+
gray: '\x1b[90m'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// CSS color codes for browser console
|
|
37
|
+
const cssColors = {
|
|
38
|
+
reset: '%c',
|
|
39
|
+
info: 'color: #2196F3; font-weight: bold;',
|
|
40
|
+
warn: 'color: #FFC107; font-weight: bold;',
|
|
41
|
+
error: 'color: #F44336; font-weight: bold;',
|
|
42
|
+
debug: 'color: #9C27B0; font-weight: bold;',
|
|
43
|
+
gray: 'color: #9E9E9E;',
|
|
44
|
+
context: 'color: #757575; font-style: italic;'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
class Logger {
|
|
48
|
+
private static instance: Logger;
|
|
49
|
+
private isDevelopment: boolean;
|
|
50
|
+
private mode: LogMode;
|
|
51
|
+
private levelEmojis: Record<LogLevel, string>;
|
|
52
|
+
|
|
53
|
+
private constructor() {
|
|
54
|
+
this.isDevelopment = process.env.NODE_ENV === 'development';
|
|
55
|
+
this.mode = (process.env.LOG_MODE as LogMode) || 'normal';
|
|
56
|
+
|
|
57
|
+
this.levelEmojis = {
|
|
58
|
+
[LogLevel.INFO]: 'ℹ️',
|
|
59
|
+
[LogLevel.WARN]: '⚠️',
|
|
60
|
+
[LogLevel.ERROR]: '❌',
|
|
61
|
+
[LogLevel.DEBUG]: '🔍'
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public static getInstance(): Logger {
|
|
66
|
+
if (!Logger.instance) {
|
|
67
|
+
Logger.instance = new Logger();
|
|
68
|
+
}
|
|
69
|
+
return Logger.instance;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public setMode(mode: LogMode) {
|
|
73
|
+
this.mode = mode;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private shouldLog(level: LogLevel): boolean {
|
|
77
|
+
const silent = [LogLevel.ERROR];
|
|
78
|
+
const minimal = [LogLevel.ERROR, LogLevel.WARN];
|
|
79
|
+
const normal = [LogLevel.ERROR, LogLevel.WARN, LogLevel.INFO];
|
|
80
|
+
|
|
81
|
+
if (this.mode === 'silent') return silent.includes(level);
|
|
82
|
+
if (this.mode === 'minimal') return minimal.includes(level);
|
|
83
|
+
if (this.mode === 'normal') return normal.includes(level);
|
|
84
|
+
return true; // verbose mode
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private formatMessage(logMessage: LogMessage): string | [string, ...string[]] {
|
|
88
|
+
const { level, message, timestamp, context } = logMessage;
|
|
89
|
+
const emoji = this.levelEmojis[level];
|
|
90
|
+
|
|
91
|
+
const timestampStr = `[${timestamp}]`;
|
|
92
|
+
const levelStr = `[${level.toUpperCase()}]`;
|
|
93
|
+
const messageStr = `${emoji} ${message}`;
|
|
94
|
+
|
|
95
|
+
const contextStr = context
|
|
96
|
+
? `\nContext: ${JSON.stringify(context, null, 2)}`
|
|
97
|
+
: '';
|
|
98
|
+
|
|
99
|
+
if (isNode) {
|
|
100
|
+
const color = ansiColors[level === LogLevel.INFO ? 'blue' :
|
|
101
|
+
level === LogLevel.WARN ? 'yellow' :
|
|
102
|
+
level === LogLevel.ERROR ? 'red' : 'magenta'];
|
|
103
|
+
|
|
104
|
+
return `${ansiColors.gray}${timestampStr}${ansiColors.reset} ${color}${levelStr}${ansiColors.reset} ${messageStr}${contextStr}`;
|
|
105
|
+
} else {
|
|
106
|
+
const color = cssColors[level];
|
|
107
|
+
return [
|
|
108
|
+
`${timestampStr} ${levelStr} ${messageStr}${contextStr}`,
|
|
109
|
+
cssColors.reset,
|
|
110
|
+
color,
|
|
111
|
+
cssColors.gray,
|
|
112
|
+
cssColors.context
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private log(level: LogLevel, message: string, context?: Record<string, any>) {
|
|
118
|
+
if (!this.shouldLog(level)) return;
|
|
119
|
+
|
|
120
|
+
const logMessage: LogMessage = {
|
|
121
|
+
level,
|
|
122
|
+
message,
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
context
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const formattedMessage = this.formatMessage(logMessage);
|
|
128
|
+
|
|
129
|
+
switch (level) {
|
|
130
|
+
case LogLevel.ERROR:
|
|
131
|
+
console.error(...(Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage]));
|
|
132
|
+
break;
|
|
133
|
+
case LogLevel.WARN:
|
|
134
|
+
console.warn(...(Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage]));
|
|
135
|
+
break;
|
|
136
|
+
case LogLevel.DEBUG:
|
|
137
|
+
if (this.isDevelopment) {
|
|
138
|
+
console.debug(...(Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage]));
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
console.log(...(Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage]));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public info(message: string, context?: Record<string, any>) {
|
|
147
|
+
this.log(LogLevel.INFO, message, context);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public warn(message: string, context?: Record<string, any>) {
|
|
151
|
+
this.log(LogLevel.WARN, message, context);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public error(message: string, context?: Record<string, any>) {
|
|
155
|
+
this.log(LogLevel.ERROR, message, context);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public debug(message: string, context?: Record<string, any>) {
|
|
159
|
+
this.log(LogLevel.DEBUG, message, context);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const logger = Logger.getInstance();
|
|
@@ -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
|
+
});
|