@studious-lms/server 1.9.0 → 1.12.0
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 +3 -0
- package/dist/index.js +40 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/config/cors.d.ts.map +1 -1
- package/dist/lib/config/cors.js +7 -2
- package/dist/lib/config/cors.js.map +1 -1
- package/dist/lib/config/env.d.ts +8 -0
- package/dist/lib/config/env.d.ts.map +1 -1
- package/dist/lib/config/env.js +5 -2
- package/dist/lib/config/env.js.map +1 -1
- package/dist/lib/pusher.d.ts +2 -1
- package/dist/lib/pusher.d.ts.map +1 -1
- package/dist/lib/pusher.js +4 -3
- package/dist/lib/pusher.js.map +1 -1
- package/dist/models/agenda.d.ts +6 -0
- package/dist/models/agenda.d.ts.map +1 -1
- package/dist/models/announcement.d.ts +3 -0
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/assignment.d.ts +61 -0
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/auth.d.ts +6 -0
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/class.d.ts +18 -0
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +5 -2
- package/dist/models/class.js.map +1 -1
- package/dist/models/comment.d.ts +6 -0
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/event.d.ts +12 -0
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/file.d.ts +38 -0
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/folder.d.ts +6 -0
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +6 -0
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/newtonChat.d.ts +3 -0
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/school.d.ts +3 -0
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/section.d.ts +3 -0
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/worksheet.d.ts +58 -0
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +26 -2
- package/dist/models/worksheet.js.map +1 -1
- package/dist/routers/_app.d.ts +6400 -4078
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +6 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/admin.d.ts +190 -0
- package/dist/routers/admin.d.ts.map +1 -0
- package/dist/routers/admin.js +50 -0
- package/dist/routers/admin.js.map +1 -0
- package/dist/routers/agenda.d.ts +6 -0
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts +2 -0
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +50 -0
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/class.d.ts +31 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +5 -2
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/comment.d.ts +2 -0
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/discussion.d.ts +823 -0
- package/dist/routers/discussion.d.ts.map +1 -0
- package/dist/routers/discussion.js +151 -0
- package/dist/routers/discussion.js.map +1 -0
- package/dist/routers/event.d.ts +3 -0
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/file.d.ts +4 -0
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/school.d.ts +6 -0
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/studentProgress.d.ts +45 -0
- package/dist/routers/studentProgress.d.ts.map +1 -1
- package/dist/routers/studentProgress.js +12 -3
- package/dist/routers/studentProgress.js.map +1 -1
- package/dist/routers/worksheet.d.ts +3 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/seedDatabase.js +1490 -1307
- package/dist/seedDatabase.js.map +1 -1
- package/dist/services/adminInstitution.d.ts +102 -0
- package/dist/services/adminInstitution.d.ts.map +1 -0
- package/dist/services/adminInstitution.js +117 -0
- package/dist/services/adminInstitution.js.map +1 -0
- package/dist/services/adminRunJob.d.ts +45 -0
- package/dist/services/adminRunJob.d.ts.map +1 -0
- package/dist/services/adminRunJob.js +41 -0
- package/dist/services/adminRunJob.js.map +1 -0
- package/dist/services/agenda.d.ts +6 -0
- package/dist/services/agenda.d.ts.map +1 -1
- package/dist/services/assignment.d.ts +50 -0
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +8 -3
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/class.d.ts +31 -0
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +14 -2
- package/dist/services/class.js.map +1 -1
- package/dist/services/comment.d.ts +2 -0
- package/dist/services/comment.d.ts.map +1 -1
- package/dist/services/discussion.d.ts +762 -0
- package/dist/services/discussion.d.ts.map +1 -0
- package/dist/services/discussion.js +625 -0
- package/dist/services/discussion.js.map +1 -0
- package/dist/services/event.d.ts +3 -0
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/file.d.ts +8 -0
- package/dist/services/file.d.ts.map +1 -1
- package/dist/services/school.d.ts +6 -0
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/studentProgress.d.ts +94 -0
- package/dist/services/studentProgress.d.ts.map +1 -1
- package/dist/services/studentProgress.js +116 -3
- package/dist/services/studentProgress.js.map +1 -1
- package/dist/services/studentProgressInferenceCache.d.ts +16 -0
- package/dist/services/studentProgressInferenceCache.d.ts.map +1 -0
- package/dist/services/studentProgressInferenceCache.js +43 -0
- package/dist/services/studentProgressInferenceCache.js.map +1 -0
- package/dist/services/worksheet.d.ts +3 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +5 -5
- package/dist/services/worksheet.js.map +1 -1
- package/dist/trpc.d.ts +12 -0
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +16 -3
- package/dist/trpc.js.map +1 -1
- package/dist/utils/adminPanelAccess.d.ts +8 -0
- package/dist/utils/adminPanelAccess.d.ts.map +1 -0
- package/dist/utils/adminPanelAccess.js +44 -0
- package/dist/utils/adminPanelAccess.js.map +1 -0
- package/package.json +1 -1
- package/prisma/migrations/20260510183000_add_discussions/migration.sql +151 -0
- package/prisma/schema.prisma +97 -0
- package/src/index.ts +39 -2
- package/src/lib/config/cors.ts +5 -0
- package/src/lib/config/env.ts +3 -0
- package/src/lib/pusher.ts +2 -1
- package/src/models/class.ts +3 -0
- package/src/models/worksheet.ts +28 -0
- package/src/routers/_app.ts +4 -0
- package/src/routers/admin.ts +70 -0
- package/src/routers/class.ts +3 -0
- package/src/routers/discussion.ts +205 -0
- package/src/routers/studentProgress.ts +21 -0
- package/src/seedDatabase.ts +1713 -1361
- package/src/services/adminInstitution.ts +140 -0
- package/src/services/adminRunJob.ts +46 -0
- package/src/services/assignment.ts +11 -1
- package/src/services/class.ts +18 -0
- package/src/services/discussion.ts +701 -0
- package/src/services/studentProgress.ts +274 -11
- package/src/services/studentProgressInferenceCache.ts +64 -0
- package/src/services/worksheet.ts +4 -2
- package/src/trpc.ts +15 -0
- package/src/utils/adminPanelAccess.ts +46 -0
- package/tests/integration/routers/discussion.test.ts +253 -0
- package/tests/unit/services/discussion.test.ts +356 -0
- package/tests/unit/services/studentProgress.test.ts +168 -2
package/dist/trpc.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trpc.js","sources":["trpc.ts"],"sourceRoot":"/","sourcesContent":["import { initTRPC, TRPCError } from '@trpc/server';\nimport { ZodError } from 'zod';\nimport { logger } from './utils/logger.js';\nimport { createLoggingMiddleware } from './middleware/logging.js';\nimport { createAuthMiddleware } from './middleware/auth.js';\nimport { Request, Response } from 'express';\nimport { z } from 'zod';\nimport { handlePrismaError, PrismaErrorInfo } from './utils/prismaErrorHandler.js';\nimport { findUserBySessionToken } from './models/auth.js';\n\ninterface CreateContextOptions {\n req: Request;\n res: Response;\n}\n\nexport type Context = {\n req: Request;\n res: Response;\n user: { id: string } | null;\n meta?: {\n classId?: string;\n institutionId?: string;\n };\n};\n\nexport const createTRPCContext = async (opts: CreateContextOptions): Promise<Context> => {\n const { req, res } = opts;\n\n const userHeader = req.headers[\"x-user\"];\n const tokenFromUserHeader = typeof userHeader === \"string\" ? userHeader : userHeader?.[0];\n const authorizationHeader = req.headers.authorization;\n const tokenFromAuthorization =\n typeof authorizationHeader === \"string\" && authorizationHeader.startsWith(\"Bearer \")\n ? authorizationHeader.slice(7)\n : null;\n const token = tokenFromUserHeader ?? tokenFromAuthorization;\n const user = token ? await findUserBySessionToken(token) : null;\n\n return {\n req,\n res,\n user,\n meta: {},\n };\n};\n\nexport const t = initTRPC.context<Context>().create({\n errorFormatter({ shape, error }) {\n // Handle Prisma errors specifically\n let prismaErrorInfo: PrismaErrorInfo | null = null;\n if (error.cause) {\n try {\n prismaErrorInfo = handlePrismaError(error.cause);\n } catch (e) {\n // If Prisma error handling fails, continue with normal error handling\n }\n }\n\n logger.error('tRPC Error', {\n code: shape.code,\n message: error.message,\n cause: error.cause,\n stack: error.stack,\n prismaError: prismaErrorInfo,\n });\n\n return {\n ...shape,\n data: {\n ...shape.data,\n zodError:\n error.cause instanceof ZodError ? error.cause.flatten() : null,\n prismaError: prismaErrorInfo,\n },\n };\n },\n});\n\n// Create middleware\nconst loggingMiddleware = createLoggingMiddleware(t);\nconst { isAuthed, isMemberInClass, isTeacherInClass } = createAuthMiddleware(t);\n\n// Base procedures\nexport const createTRPCRouter = t.router;\nexport const publicProcedure = t.procedure.use(loggingMiddleware);\n\n\n// Protected procedures\nexport const protectedProcedure = publicProcedure.use(isAuthed);\n\nexport const protectedClassMemberProcedure = protectedProcedure\n .input(z.object({ classId: z.string() }).passthrough())\n .use(isMemberInClass);\n\nexport const protectedTeacherProcedure = protectedProcedure\n .input(z.object({ classId: z.string() }).passthrough())\n .use(isTeacherInClass);\n\n\n// Create caller factory\nexport const createCallerFactory = t.createCallerFactory; "],"names":[],"mappings":";;AAAA,OAAO,EAAE,QAAQ,
|
|
1
|
+
{"version":3,"file":"trpc.js","sources":["trpc.ts"],"sourceRoot":"/","sourcesContent":["import { initTRPC, TRPCError } from '@trpc/server';\nimport { ZodError } from 'zod';\nimport { logger } from './utils/logger.js';\nimport { createLoggingMiddleware } from './middleware/logging.js';\nimport { createAuthMiddleware } from './middleware/auth.js';\nimport { Request, Response } from 'express';\nimport { z } from 'zod';\nimport { handlePrismaError, PrismaErrorInfo } from './utils/prismaErrorHandler.js';\nimport { findUserBySessionToken } from './models/auth.js';\nimport { assertAdminPanelUser } from './utils/adminPanelAccess.js';\n\ninterface CreateContextOptions {\n req: Request;\n res: Response;\n}\n\nexport type Context = {\n req: Request;\n res: Response;\n user: { id: string } | null;\n meta?: {\n classId?: string;\n institutionId?: string;\n };\n};\n\nexport const createTRPCContext = async (opts: CreateContextOptions): Promise<Context> => {\n const { req, res } = opts;\n\n const userHeader = req.headers[\"x-user\"];\n const tokenFromUserHeader = typeof userHeader === \"string\" ? userHeader : userHeader?.[0];\n const authorizationHeader = req.headers.authorization;\n const tokenFromAuthorization =\n typeof authorizationHeader === \"string\" && authorizationHeader.startsWith(\"Bearer \")\n ? authorizationHeader.slice(7)\n : null;\n const token = tokenFromUserHeader ?? tokenFromAuthorization;\n const user = token ? await findUserBySessionToken(token) : null;\n\n return {\n req,\n res,\n user,\n meta: {},\n };\n};\n\nexport const t = initTRPC.context<Context>().create({\n errorFormatter({ shape, error }) {\n // Handle Prisma errors specifically\n let prismaErrorInfo: PrismaErrorInfo | null = null;\n if (error.cause) {\n try {\n prismaErrorInfo = handlePrismaError(error.cause);\n } catch (e) {\n // If Prisma error handling fails, continue with normal error handling\n }\n }\n\n logger.error('tRPC Error', {\n code: shape.code,\n message: error.message,\n cause: error.cause,\n stack: error.stack,\n prismaError: prismaErrorInfo,\n });\n\n return {\n ...shape,\n data: {\n ...shape.data,\n zodError:\n error.cause instanceof ZodError ? error.cause.flatten() : null,\n prismaError: prismaErrorInfo,\n },\n };\n },\n});\n\n// Create middleware\nconst loggingMiddleware = createLoggingMiddleware(t);\nconst { isAuthed, isMemberInClass, isTeacherInClass } = createAuthMiddleware(t);\n\n// Base procedures\nexport const createTRPCRouter = t.router;\nexport const publicProcedure = t.procedure.use(loggingMiddleware);\n\n\n// Protected procedures\nexport const protectedProcedure = publicProcedure.use(isAuthed);\n\nconst adminPanelMiddleware = t.middleware(({ ctx, next }) => {\n if (!ctx.user) {\n throw new TRPCError({\n code: 'UNAUTHORIZED',\n message: 'Not authenticated',\n });\n }\n assertAdminPanelUser(ctx.user.id);\n return next({ ctx });\n});\n\n/** Authenticated + env allowlist (see `assertAdminPanelUser`). */\nexport const adminProcedure = protectedProcedure.use(adminPanelMiddleware);\n\nexport const protectedClassMemberProcedure = protectedProcedure\n .input(z.object({ classId: z.string() }).passthrough())\n .use(isMemberInClass);\n\nexport const protectedTeacherProcedure = protectedProcedure\n .input(z.object({ classId: z.string() }).passthrough())\n .use(isTeacherInClass);\n\n\n// Create caller factory\nexport const createCallerFactory = t.createCallerFactory; "],"names":[],"mappings":";;AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAmB,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAiBnE,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EAAE,IAA0B,EAAoB,EAAE;IACtF,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAE1B,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzC,MAAM,mBAAmB,GAAG,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1F,MAAM,mBAAmB,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IACtD,MAAM,sBAAsB,GAC1B,OAAO,mBAAmB,KAAK,QAAQ,IAAI,mBAAmB,CAAC,UAAU,CAAC,SAAS,CAAC;QAClF,CAAC,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9B,CAAC,CAAC,IAAI,CAAC;IACX,MAAM,KAAK,GAAG,mBAAmB,IAAI,sBAAsB,CAAC;IAC5D,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEhE,OAAO;QACL,GAAG;QACH,GAAG;QACH,IAAI;QACJ,IAAI,EAAE,EAAE;KACT,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,CAAC,GAAG,QAAQ,CAAC,OAAO,EAAW,CAAC,MAAM,CAAC;IAClD,cAAc,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE;QAC7B,oCAAoC;QACpC,IAAI,eAAe,GAA2B,IAAI,CAAC;QACnD,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC;gBACH,eAAe,GAAG,iBAAiB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACnD,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,sEAAsE;YACxE,CAAC;QACH,CAAC;QAED,MAAM,CAAC,KAAK,CAAC,YAAY,EAAE;YACzB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,WAAW,EAAE,eAAe;SAC7B,CAAC,CAAC;QAEH,OAAO;YACL,GAAG,KAAK;YACR,IAAI,EAAE;gBACJ,GAAG,KAAK,CAAC,IAAI;gBACb,QAAQ,EACN,KAAK,CAAC,KAAK,YAAY,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI;gBAChE,WAAW,EAAE,eAAe;aAC7B;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC;AAEH,oBAAoB;AACpB,MAAM,iBAAiB,GAAG,uBAAuB,CAAC,CAAC,CAAC,CAAC;AACrD,MAAM,EAAE,QAAQ,EAAE,eAAe,EAAE,gBAAgB,EAAE,GAAG,oBAAoB,CAAC,CAAC,CAAC,CAAC;AAEhF,kBAAkB;AAClB,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;AACzC,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;AAGlE,uBAAuB;AACvB,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AAEhE,MAAM,oBAAoB,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE;IAC1D,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACd,MAAM,IAAI,SAAS,CAAC;YAClB,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,mBAAmB;SAC7B,CAAC,CAAC;IACL,CAAC;IACD,oBAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC;AAEH,kEAAkE;AAClE,MAAM,CAAC,MAAM,cAAc,GAAG,kBAAkB,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;AAE3E,MAAM,CAAC,MAAM,6BAA6B,GAAG,kBAAkB;KAC5D,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;KACtD,GAAG,CAAC,eAAe,CAAC,CAAC;AAExB,MAAM,CAAC,MAAM,yBAAyB,GAAG,kBAAkB;KACxD,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;KACtD,GAAG,CAAC,gBAAgB,CAAC,CAAC;AAGzB,wBAAwB;AACxB,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,mBAAmB,CAAC","debug_id":"29e1f1ae-1333-5312-bcdb-8c944e9f0f07"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin tRPC routes: in production `ADMIN_PANEL_USER_IDS` must be a non-empty
|
|
3
|
+
* comma/whitespace-separated list of user UUIDs. In non-production, an empty
|
|
4
|
+
* unset value allows any authenticated user (local ops); a non-empty list is
|
|
5
|
+
* still enforced.
|
|
6
|
+
*/
|
|
7
|
+
export declare function assertAdminPanelUser(userId: string): void;
|
|
8
|
+
//# sourceMappingURL=adminPanelAccess.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adminPanelAccess.d.ts","sourceRoot":"/","sources":["utils/adminPanelAccess.ts"],"names":[],"mappings":"AAaA;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CA0BzD"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
|
|
2
|
+
!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]="c58bf633-ef92-5c8c-bfe2-f50882cb800c")}catch(e){}}();
|
|
3
|
+
import { TRPCError } from '@trpc/server';
|
|
4
|
+
import { env } from '../lib/config/env.js';
|
|
5
|
+
function parseAdminUserIds(raw) {
|
|
6
|
+
if (!raw?.trim())
|
|
7
|
+
return new Set();
|
|
8
|
+
return new Set(raw
|
|
9
|
+
.split(/[\s,]+/)
|
|
10
|
+
.map((s) => s.trim())
|
|
11
|
+
.filter(Boolean));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Admin tRPC routes: in production `ADMIN_PANEL_USER_IDS` must be a non-empty
|
|
15
|
+
* comma/whitespace-separated list of user UUIDs. In non-production, an empty
|
|
16
|
+
* unset value allows any authenticated user (local ops); a non-empty list is
|
|
17
|
+
* still enforced.
|
|
18
|
+
*/
|
|
19
|
+
export function assertAdminPanelUser(userId) {
|
|
20
|
+
const allowed = parseAdminUserIds(env.ADMIN_PANEL_USER_IDS);
|
|
21
|
+
if (env.NODE_ENV === 'production') {
|
|
22
|
+
if (allowed.size === 0) {
|
|
23
|
+
throw new TRPCError({
|
|
24
|
+
code: 'FORBIDDEN',
|
|
25
|
+
message: 'Admin API disabled: set ADMIN_PANEL_USER_IDS to a non-empty list of user ids.',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (!allowed.has(userId)) {
|
|
29
|
+
throw new TRPCError({
|
|
30
|
+
code: 'FORBIDDEN',
|
|
31
|
+
message: 'You are not allowed to use the admin API.',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (allowed.size > 0 && !allowed.has(userId)) {
|
|
37
|
+
throw new TRPCError({
|
|
38
|
+
code: 'FORBIDDEN',
|
|
39
|
+
message: 'You are not allowed to use the admin API.',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=adminPanelAccess.js.map
|
|
44
|
+
//# debugId=c58bf633-ef92-5c8c-bfe2-f50882cb800c
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adminPanelAccess.js","sources":["utils/adminPanelAccess.ts"],"sourceRoot":"/","sourcesContent":["import { TRPCError } from '@trpc/server';\nimport { env } from '../lib/config/env.js';\n\nfunction parseAdminUserIds(raw: string | undefined): Set<string> {\n if (!raw?.trim()) return new Set();\n return new Set(\n raw\n .split(/[\\s,]+/)\n .map((s) => s.trim())\n .filter(Boolean),\n );\n}\n\n/**\n * Admin tRPC routes: in production `ADMIN_PANEL_USER_IDS` must be a non-empty\n * comma/whitespace-separated list of user UUIDs. In non-production, an empty\n * unset value allows any authenticated user (local ops); a non-empty list is\n * still enforced.\n */\nexport function assertAdminPanelUser(userId: string): void {\n const allowed = parseAdminUserIds(env.ADMIN_PANEL_USER_IDS);\n\n if (env.NODE_ENV === 'production') {\n if (allowed.size === 0) {\n throw new TRPCError({\n code: 'FORBIDDEN',\n message:\n 'Admin API disabled: set ADMIN_PANEL_USER_IDS to a non-empty list of user ids.',\n });\n }\n if (!allowed.has(userId)) {\n throw new TRPCError({\n code: 'FORBIDDEN',\n message: 'You are not allowed to use the admin API.',\n });\n }\n return;\n }\n\n if (allowed.size > 0 && !allowed.has(userId)) {\n throw new TRPCError({\n code: 'FORBIDDEN',\n message: 'You are not allowed to use the admin API.',\n });\n }\n}\n"],"names":[],"mappings":";;AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAE3C,SAAS,iBAAiB,CAAC,GAAuB;IAChD,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE;QAAE,OAAO,IAAI,GAAG,EAAE,CAAC;IACnC,OAAO,IAAI,GAAG,CACZ,GAAG;SACA,KAAK,CAAC,QAAQ,CAAC;SACf,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CACnB,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAc;IACjD,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IAE5D,IAAI,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;QAClC,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,SAAS,CAAC;gBAClB,IAAI,EAAE,WAAW;gBACjB,OAAO,EACL,+EAA+E;aAClF,CAAC,CAAC;QACL,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,SAAS,CAAC;gBAClB,IAAI,EAAE,WAAW;gBACjB,OAAO,EAAE,2CAA2C;aACrD,CAAC,CAAC;QACL,CAAC;QACD,OAAO;IACT,CAAC;IAED,IAAI,OAAO,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,SAAS,CAAC;YAClB,IAAI,EAAE,WAAW;YACjB,OAAO,EAAE,2CAA2C;SACrD,CAAC,CAAC;IACL,CAAC;AACH,CAAC","debug_id":"c58bf633-ef92-5c8c-bfe2-f50882cb800c"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
-- CreateEnum
|
|
2
|
+
CREATE TYPE "DiscussionType" AS ENUM ('DISCUSSION', 'QUESTION', 'ANNOUNCEMENT');
|
|
3
|
+
|
|
4
|
+
-- AlterTable
|
|
5
|
+
ALTER TABLE "Class" ADD COLUMN "discussionsEnabled" BOOLEAN NOT NULL DEFAULT true;
|
|
6
|
+
ALTER TABLE "Class" ADD COLUMN "studentsCanCreateDiscussions" BOOLEAN NOT NULL DEFAULT true;
|
|
7
|
+
ALTER TABLE "Class" ADD COLUMN "studentsCanReply" BOOLEAN NOT NULL DEFAULT true;
|
|
8
|
+
|
|
9
|
+
-- AlterTable
|
|
10
|
+
ALTER TABLE "File" ADD COLUMN "discussionId" TEXT;
|
|
11
|
+
ALTER TABLE "File" ADD COLUMN "discussionReplyId" TEXT;
|
|
12
|
+
|
|
13
|
+
-- AlterTable
|
|
14
|
+
ALTER TABLE "Reaction" ADD COLUMN "discussionId" TEXT;
|
|
15
|
+
ALTER TABLE "Reaction" ADD COLUMN "discussionReplyId" TEXT;
|
|
16
|
+
|
|
17
|
+
-- CreateTable
|
|
18
|
+
CREATE TABLE "Discussion" (
|
|
19
|
+
"id" TEXT NOT NULL,
|
|
20
|
+
"classId" TEXT NOT NULL,
|
|
21
|
+
"authorId" TEXT NOT NULL,
|
|
22
|
+
"title" TEXT NOT NULL,
|
|
23
|
+
"body" TEXT NOT NULL,
|
|
24
|
+
"type" "DiscussionType" NOT NULL DEFAULT 'DISCUSSION',
|
|
25
|
+
"isPinned" BOOLEAN NOT NULL DEFAULT false,
|
|
26
|
+
"isLocked" BOOLEAN NOT NULL DEFAULT false,
|
|
27
|
+
"allowReplies" BOOLEAN NOT NULL DEFAULT true,
|
|
28
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
29
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
30
|
+
"lastActivityAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
31
|
+
|
|
32
|
+
CONSTRAINT "Discussion_pkey" PRIMARY KEY ("id")
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
-- CreateTable
|
|
36
|
+
CREATE TABLE "DiscussionReply" (
|
|
37
|
+
"id" TEXT NOT NULL,
|
|
38
|
+
"discussionId" TEXT NOT NULL,
|
|
39
|
+
"authorId" TEXT NOT NULL,
|
|
40
|
+
"parentId" TEXT,
|
|
41
|
+
"body" TEXT NOT NULL,
|
|
42
|
+
"isAcceptedAnswer" BOOLEAN NOT NULL DEFAULT false,
|
|
43
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
44
|
+
"modifiedAt" TIMESTAMP(3),
|
|
45
|
+
"deletedAt" TIMESTAMP(3),
|
|
46
|
+
|
|
47
|
+
CONSTRAINT "DiscussionReply_pkey" PRIMARY KEY ("id")
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
-- CreateTable
|
|
51
|
+
CREATE TABLE "DiscussionTag" (
|
|
52
|
+
"id" TEXT NOT NULL,
|
|
53
|
+
"classId" TEXT NOT NULL,
|
|
54
|
+
"name" TEXT NOT NULL,
|
|
55
|
+
"color" TEXT NOT NULL DEFAULT '#3B82F6',
|
|
56
|
+
|
|
57
|
+
CONSTRAINT "DiscussionTag_pkey" PRIMARY KEY ("id")
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
-- CreateTable
|
|
61
|
+
CREATE TABLE "DiscussionRead" (
|
|
62
|
+
"userId" TEXT NOT NULL,
|
|
63
|
+
"discussionId" TEXT NOT NULL,
|
|
64
|
+
"lastReadAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
65
|
+
|
|
66
|
+
CONSTRAINT "DiscussionRead_pkey" PRIMARY KEY ("userId","discussionId")
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
-- CreateTable
|
|
70
|
+
CREATE TABLE "_DiscussionToTag" (
|
|
71
|
+
"A" TEXT NOT NULL,
|
|
72
|
+
"B" TEXT NOT NULL
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
-- CreateIndex
|
|
76
|
+
CREATE INDEX "Discussion_classId_isPinned_lastActivityAt_idx" ON "Discussion"("classId", "isPinned", "lastActivityAt");
|
|
77
|
+
|
|
78
|
+
-- CreateIndex
|
|
79
|
+
CREATE INDEX "Discussion_classId_createdAt_idx" ON "Discussion"("classId", "createdAt");
|
|
80
|
+
|
|
81
|
+
-- CreateIndex
|
|
82
|
+
CREATE INDEX "DiscussionReply_discussionId_createdAt_idx" ON "DiscussionReply"("discussionId", "createdAt");
|
|
83
|
+
|
|
84
|
+
-- CreateIndex
|
|
85
|
+
CREATE INDEX "DiscussionReply_parentId_idx" ON "DiscussionReply"("parentId");
|
|
86
|
+
|
|
87
|
+
-- CreateIndex
|
|
88
|
+
CREATE UNIQUE INDEX "DiscussionTag_classId_name_key" ON "DiscussionTag"("classId", "name");
|
|
89
|
+
|
|
90
|
+
-- CreateIndex
|
|
91
|
+
CREATE INDEX "DiscussionRead_userId_idx" ON "DiscussionRead"("userId");
|
|
92
|
+
|
|
93
|
+
-- CreateIndex
|
|
94
|
+
CREATE UNIQUE INDEX "_DiscussionToTag_AB_unique" ON "_DiscussionToTag"("A", "B");
|
|
95
|
+
|
|
96
|
+
-- CreateIndex
|
|
97
|
+
CREATE INDEX "_DiscussionToTag_B_index" ON "_DiscussionToTag"("B");
|
|
98
|
+
|
|
99
|
+
-- CreateIndex
|
|
100
|
+
CREATE UNIQUE INDEX "Reaction_userId_discussionId_key" ON "Reaction"("userId", "discussionId");
|
|
101
|
+
|
|
102
|
+
-- CreateIndex
|
|
103
|
+
CREATE UNIQUE INDEX "Reaction_userId_discussionReplyId_key" ON "Reaction"("userId", "discussionReplyId");
|
|
104
|
+
|
|
105
|
+
-- CreateIndex
|
|
106
|
+
CREATE INDEX "Reaction_discussionId_idx" ON "Reaction"("discussionId");
|
|
107
|
+
|
|
108
|
+
-- CreateIndex
|
|
109
|
+
CREATE INDEX "Reaction_discussionReplyId_idx" ON "Reaction"("discussionReplyId");
|
|
110
|
+
|
|
111
|
+
-- AddForeignKey
|
|
112
|
+
ALTER TABLE "File" ADD CONSTRAINT "File_discussionId_fkey" FOREIGN KEY ("discussionId") REFERENCES "Discussion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
113
|
+
|
|
114
|
+
-- AddForeignKey
|
|
115
|
+
ALTER TABLE "File" ADD CONSTRAINT "File_discussionReplyId_fkey" FOREIGN KEY ("discussionReplyId") REFERENCES "DiscussionReply"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
116
|
+
|
|
117
|
+
-- AddForeignKey
|
|
118
|
+
ALTER TABLE "Discussion" ADD CONSTRAINT "Discussion_classId_fkey" FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
119
|
+
|
|
120
|
+
-- AddForeignKey
|
|
121
|
+
ALTER TABLE "Discussion" ADD CONSTRAINT "Discussion_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
122
|
+
|
|
123
|
+
-- AddForeignKey
|
|
124
|
+
ALTER TABLE "DiscussionReply" ADD CONSTRAINT "DiscussionReply_discussionId_fkey" FOREIGN KEY ("discussionId") REFERENCES "Discussion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
125
|
+
|
|
126
|
+
-- AddForeignKey
|
|
127
|
+
ALTER TABLE "DiscussionReply" ADD CONSTRAINT "DiscussionReply_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
128
|
+
|
|
129
|
+
-- AddForeignKey
|
|
130
|
+
ALTER TABLE "DiscussionReply" ADD CONSTRAINT "DiscussionReply_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "DiscussionReply"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
131
|
+
|
|
132
|
+
-- AddForeignKey
|
|
133
|
+
ALTER TABLE "DiscussionTag" ADD CONSTRAINT "DiscussionTag_classId_fkey" FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
134
|
+
|
|
135
|
+
-- AddForeignKey
|
|
136
|
+
ALTER TABLE "DiscussionRead" ADD CONSTRAINT "DiscussionRead_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
137
|
+
|
|
138
|
+
-- AddForeignKey
|
|
139
|
+
ALTER TABLE "DiscussionRead" ADD CONSTRAINT "DiscussionRead_discussionId_fkey" FOREIGN KEY ("discussionId") REFERENCES "Discussion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
140
|
+
|
|
141
|
+
-- AddForeignKey
|
|
142
|
+
ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_discussionId_fkey" FOREIGN KEY ("discussionId") REFERENCES "Discussion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
143
|
+
|
|
144
|
+
-- AddForeignKey
|
|
145
|
+
ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_discussionReplyId_fkey" FOREIGN KEY ("discussionReplyId") REFERENCES "DiscussionReply"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
146
|
+
|
|
147
|
+
-- AddForeignKey
|
|
148
|
+
ALTER TABLE "_DiscussionToTag" ADD CONSTRAINT "_DiscussionToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "Discussion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
149
|
+
|
|
150
|
+
-- AddForeignKey
|
|
151
|
+
ALTER TABLE "_DiscussionToTag" ADD CONSTRAINT "_DiscussionToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "DiscussionTag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
package/prisma/schema.prisma
CHANGED
|
@@ -84,6 +84,12 @@ enum ReactionType {
|
|
|
84
84
|
HAPPY
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
enum DiscussionType {
|
|
88
|
+
DISCUSSION
|
|
89
|
+
QUESTION
|
|
90
|
+
ANNOUNCEMENT
|
|
91
|
+
}
|
|
92
|
+
|
|
87
93
|
enum SubmissionRecommendationState {
|
|
88
94
|
NONE
|
|
89
95
|
OPEN
|
|
@@ -227,6 +233,9 @@ model User {
|
|
|
227
233
|
createdLabChats LabChat[] @relation("CreatedLabChats")
|
|
228
234
|
comments Comment[]
|
|
229
235
|
reactions Reaction[]
|
|
236
|
+
discussions Discussion[]
|
|
237
|
+
discussionReplies DiscussionReply[]
|
|
238
|
+
discussionReads DiscussionRead[]
|
|
230
239
|
}
|
|
231
240
|
|
|
232
241
|
model SchoolMembership {
|
|
@@ -311,6 +320,12 @@ model Class {
|
|
|
311
320
|
departmentId String?
|
|
312
321
|
tags Tag[] @relation("ClassTags")
|
|
313
322
|
permissions Permission[] @relation("ClassPermissions")
|
|
323
|
+
discussions Discussion[]
|
|
324
|
+
discussionTags DiscussionTag[]
|
|
325
|
+
|
|
326
|
+
discussionsEnabled Boolean @default(true)
|
|
327
|
+
studentsCanCreateDiscussions Boolean @default(true)
|
|
328
|
+
studentsCanReply Boolean @default(true)
|
|
314
329
|
|
|
315
330
|
@@index([schoolId])
|
|
316
331
|
@@index([departmentId])
|
|
@@ -422,6 +437,12 @@ model File {
|
|
|
422
437
|
announcement Announcement? @relation("AnnouncementAttachments", fields: [announcementId], references: [id], onDelete: Cascade)
|
|
423
438
|
announcementId String?
|
|
424
439
|
|
|
440
|
+
discussion Discussion? @relation("DiscussionFiles", fields: [discussionId], references: [id], onDelete: Cascade)
|
|
441
|
+
discussionId String?
|
|
442
|
+
|
|
443
|
+
discussionReply DiscussionReply? @relation("DiscussionReplyFiles", fields: [discussionReplyId], references: [id], onDelete: Cascade)
|
|
444
|
+
discussionReplyId String?
|
|
445
|
+
|
|
425
446
|
schools School[] @relation("SchoolLogo")
|
|
426
447
|
|
|
427
448
|
schoolDevelopementProgram SchoolDevelopementProgram? @relation("SchoolDevelopementProgramSupportingDocumentation", fields: [schoolDevelopementProgramId], references: [id], onDelete: Cascade)
|
|
@@ -507,6 +528,74 @@ model Comment {
|
|
|
507
528
|
@@index([submissionId])
|
|
508
529
|
}
|
|
509
530
|
|
|
531
|
+
model Discussion {
|
|
532
|
+
id String @id @default(uuid())
|
|
533
|
+
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
534
|
+
classId String
|
|
535
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
536
|
+
authorId String
|
|
537
|
+
title String
|
|
538
|
+
body String
|
|
539
|
+
type DiscussionType @default(DISCUSSION)
|
|
540
|
+
isPinned Boolean @default(false)
|
|
541
|
+
isLocked Boolean @default(false)
|
|
542
|
+
allowReplies Boolean @default(true)
|
|
543
|
+
attachments File[] @relation("DiscussionFiles")
|
|
544
|
+
tags DiscussionTag[] @relation("DiscussionToTag")
|
|
545
|
+
replies DiscussionReply[]
|
|
546
|
+
reactions Reaction[]
|
|
547
|
+
reads DiscussionRead[]
|
|
548
|
+
createdAt DateTime @default(now())
|
|
549
|
+
updatedAt DateTime @updatedAt
|
|
550
|
+
lastActivityAt DateTime @default(now())
|
|
551
|
+
|
|
552
|
+
@@index([classId, isPinned, lastActivityAt])
|
|
553
|
+
@@index([classId, createdAt])
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
model DiscussionReply {
|
|
557
|
+
id String @id @default(uuid())
|
|
558
|
+
discussion Discussion @relation(fields: [discussionId], references: [id], onDelete: Cascade)
|
|
559
|
+
discussionId String
|
|
560
|
+
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
561
|
+
authorId String
|
|
562
|
+
parent DiscussionReply? @relation("ReplyTree", fields: [parentId], references: [id], onDelete: Cascade)
|
|
563
|
+
parentId String?
|
|
564
|
+
children DiscussionReply[] @relation("ReplyTree")
|
|
565
|
+
body String
|
|
566
|
+
attachments File[] @relation("DiscussionReplyFiles")
|
|
567
|
+
reactions Reaction[]
|
|
568
|
+
isAcceptedAnswer Boolean @default(false)
|
|
569
|
+
createdAt DateTime @default(now())
|
|
570
|
+
modifiedAt DateTime? @updatedAt
|
|
571
|
+
deletedAt DateTime?
|
|
572
|
+
|
|
573
|
+
@@index([discussionId, createdAt])
|
|
574
|
+
@@index([parentId])
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
model DiscussionTag {
|
|
578
|
+
id String @id @default(uuid())
|
|
579
|
+
class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
|
|
580
|
+
classId String
|
|
581
|
+
name String
|
|
582
|
+
color String @default("#3B82F6")
|
|
583
|
+
discussions Discussion[] @relation("DiscussionToTag")
|
|
584
|
+
|
|
585
|
+
@@unique([classId, name])
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
model DiscussionRead {
|
|
589
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
590
|
+
userId String
|
|
591
|
+
discussion Discussion @relation(fields: [discussionId], references: [id], onDelete: Cascade)
|
|
592
|
+
discussionId String
|
|
593
|
+
lastReadAt DateTime @default(now())
|
|
594
|
+
|
|
595
|
+
@@id([userId, discussionId])
|
|
596
|
+
@@index([userId])
|
|
597
|
+
}
|
|
598
|
+
|
|
510
599
|
model Reaction {
|
|
511
600
|
id String @id @default(uuid())
|
|
512
601
|
type ReactionType
|
|
@@ -516,12 +605,20 @@ model Reaction {
|
|
|
516
605
|
announcementId String?
|
|
517
606
|
comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
|
518
607
|
commentId String?
|
|
608
|
+
discussion Discussion? @relation(fields: [discussionId], references: [id], onDelete: Cascade)
|
|
609
|
+
discussionId String?
|
|
610
|
+
discussionReply DiscussionReply? @relation(fields: [discussionReplyId], references: [id], onDelete: Cascade)
|
|
611
|
+
discussionReplyId String?
|
|
519
612
|
createdAt DateTime @default(now())
|
|
520
613
|
|
|
521
614
|
@@unique([userId, announcementId])
|
|
522
615
|
@@unique([userId, commentId])
|
|
616
|
+
@@unique([userId, discussionId])
|
|
617
|
+
@@unique([userId, discussionReplyId])
|
|
523
618
|
@@index([announcementId])
|
|
524
619
|
@@index([commentId])
|
|
620
|
+
@@index([discussionId])
|
|
621
|
+
@@index([discussionReplyId])
|
|
525
622
|
}
|
|
526
623
|
|
|
527
624
|
model Submission {
|
package/src/index.ts
CHANGED
|
@@ -58,7 +58,8 @@ app.use(cors({
|
|
|
58
58
|
},
|
|
59
59
|
credentials: true,
|
|
60
60
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
61
|
-
allowedHeaders
|
|
61
|
+
// Omit allowedHeaders so `cors` echoes `Access-Control-Request-Headers` on preflight.
|
|
62
|
+
// A fixed list breaks browsers that add headers (e.g. Sentry `baggage`, `sentry-trace`).
|
|
62
63
|
preflightContinue: false,
|
|
63
64
|
optionsSuccessStatus: 204,
|
|
64
65
|
}));
|
|
@@ -221,6 +222,38 @@ app.post('/api/pusher/auth', async (req, res) => {
|
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
224
|
|
|
225
|
+
if (channel_name.startsWith('private-class-') && channel_name.endsWith('-discussions')) {
|
|
226
|
+
const classId = channel_name
|
|
227
|
+
.replace('private-class-', '')
|
|
228
|
+
.replace('-discussions', '');
|
|
229
|
+
const isMember = await prisma.class.findFirst({
|
|
230
|
+
where: {
|
|
231
|
+
id: classId,
|
|
232
|
+
discussionsEnabled: true,
|
|
233
|
+
OR: [
|
|
234
|
+
{ students: { some: { id: user.id } } },
|
|
235
|
+
{ teachers: { some: { id: user.id } } },
|
|
236
|
+
{
|
|
237
|
+
school: {
|
|
238
|
+
is: {
|
|
239
|
+
memberships: {
|
|
240
|
+
some: {
|
|
241
|
+
userId: user.id,
|
|
242
|
+
role: 'ADMIN',
|
|
243
|
+
status: 'ACTIVE',
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
if (!isMember) {
|
|
253
|
+
return res.status(403).json({ error: 'Not a member of this class discussion channel' });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
224
257
|
// Verify channel access for private-class-* channels
|
|
225
258
|
// if (channel_name.startsWith('private-class-')) {
|
|
226
259
|
// const classId = channel_name.replace('private-class-', '');
|
|
@@ -586,7 +619,11 @@ app.options('/trpc/*', (req, res) => {
|
|
|
586
619
|
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
587
620
|
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
588
621
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
589
|
-
|
|
622
|
+
const requestedHeaders = req.headers['access-control-request-headers'];
|
|
623
|
+
if (requestedHeaders) {
|
|
624
|
+
res.setHeader('Access-Control-Allow-Headers', requestedHeaders);
|
|
625
|
+
res.setHeader('Vary', 'Origin, Access-Control-Request-Headers');
|
|
626
|
+
}
|
|
590
627
|
}
|
|
591
628
|
res.sendStatus(204);
|
|
592
629
|
});
|
package/src/lib/config/cors.ts
CHANGED
|
@@ -9,6 +9,11 @@ const productionOrigins = [
|
|
|
9
9
|
'https://www.studious.sh',
|
|
10
10
|
'https://studious.sh',
|
|
11
11
|
'https://dev.studious.sh',
|
|
12
|
+
// @todo: remove in actual prod
|
|
13
|
+
'http://localhost:3000',
|
|
14
|
+
'http://localhost:3001',
|
|
15
|
+
'http://127.0.0.1:3000',
|
|
16
|
+
'http://127.0.0.1:3001',
|
|
12
17
|
];
|
|
13
18
|
|
|
14
19
|
const nonProductionOrigins = [
|
package/src/lib/config/env.ts
CHANGED
|
@@ -63,6 +63,8 @@ const fullSchema = baseSchema.extend({
|
|
|
63
63
|
INFERENCE_API_BASE_URL: z.string().url().optional(),
|
|
64
64
|
OPENAI_API_KEY: z.string().optional(),
|
|
65
65
|
LOG_MODE: z.enum(['normal', 'verbose', 'quiet']).default('normal'),
|
|
66
|
+
/** Comma/whitespace-separated user UUIDs allowed to call `admin.*` tRPC (required in production). */
|
|
67
|
+
ADMIN_PANEL_USER_IDS: z.string().optional(),
|
|
66
68
|
});
|
|
67
69
|
|
|
68
70
|
// Test schema - only require what's needed for tests
|
|
@@ -91,6 +93,7 @@ const testSchema = baseSchema.extend({
|
|
|
91
93
|
OPENAI_API_KEY: z.string().optional(),
|
|
92
94
|
INFERENCE_API_BASE_URL: z.string().url().optional(),
|
|
93
95
|
LOG_MODE: z.enum(['normal', 'verbose', 'quiet']).default('quiet'),
|
|
96
|
+
ADMIN_PANEL_USER_IDS: z.string().optional(),
|
|
94
97
|
});
|
|
95
98
|
|
|
96
99
|
// Use test schema in test mode, full schema otherwise
|
package/src/lib/pusher.ts
CHANGED
|
@@ -12,6 +12,7 @@ const pusher = new Pusher({
|
|
|
12
12
|
const chatChannel = (conversationId: string) => `private-conversation-${conversationId}`;
|
|
13
13
|
const worksheetChannel = (worksheetResponseId: string) => `private-worksheet-${worksheetResponseId}`;
|
|
14
14
|
const teacherChannel = (classId: string) => `private-teacher-${classId}`;
|
|
15
|
+
const discussionChannel = (classId: string) => `private-class-${classId}-discussions`;
|
|
15
16
|
/**
|
|
16
17
|
* Per-worksheet job channel used for asynchronous worksheet creation
|
|
17
18
|
* (e.g. PDF → worksheet conversion). Distinct prefix from the student
|
|
@@ -19,4 +20,4 @@ const teacherChannel = (classId: string) => `private-teacher-${classId}`;
|
|
|
19
20
|
*/
|
|
20
21
|
const worksheetJobChannel = (worksheetId: string) => `private-worksheet-job-${worksheetId}`;
|
|
21
22
|
|
|
22
|
-
export { pusher, chatChannel, worksheetChannel, teacherChannel, worksheetJobChannel };
|
|
23
|
+
export { pusher, chatChannel, worksheetChannel, teacherChannel, discussionChannel, worksheetJobChannel };
|
package/src/models/class.ts
CHANGED
package/src/models/worksheet.ts
CHANGED
|
@@ -456,6 +456,34 @@ export function findStudentQuestionProgressByIdForTeacherAndResponse(
|
|
|
456
456
|
});
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
+
/**
|
|
460
|
+
* @returns Student question progress if requester is the answering student (class member) or a
|
|
461
|
+
* class teacher / school admin for that worksheet.
|
|
462
|
+
*/
|
|
463
|
+
export function findStudentQuestionProgressByIdForStudentOrTeacher(
|
|
464
|
+
id: string,
|
|
465
|
+
userId: string
|
|
466
|
+
) {
|
|
467
|
+
return prisma.studentQuestionProgress.findFirst({
|
|
468
|
+
where: {
|
|
469
|
+
id,
|
|
470
|
+
OR: [
|
|
471
|
+
{
|
|
472
|
+
studentId: userId,
|
|
473
|
+
question: { worksheet: worksheetClassMemberWhere(userId) },
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
question: { worksheet: worksheetTeacherWhere(userId) },
|
|
477
|
+
},
|
|
478
|
+
],
|
|
479
|
+
},
|
|
480
|
+
include: {
|
|
481
|
+
question: true,
|
|
482
|
+
studentWorksheetResponse: true,
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
459
487
|
/** Update student's question progress. */
|
|
460
488
|
export function updateStudentQuestionProgress(
|
|
461
489
|
id: string,
|
package/src/routers/_app.ts
CHANGED
|
@@ -26,6 +26,8 @@ import { schoolRouter } from "./school.js";
|
|
|
26
26
|
import { departmentRouter } from "./department.js";
|
|
27
27
|
import { tagRouter } from "./tag.js";
|
|
28
28
|
import { permissionsRouter } from "./permissions.js";
|
|
29
|
+
import { discussionRouter } from "./discussion.js";
|
|
30
|
+
import { adminRouter } from "./admin.js";
|
|
29
31
|
|
|
30
32
|
export const appRouter = createTRPCRouter({
|
|
31
33
|
class: classRouter,
|
|
@@ -53,6 +55,8 @@ export const appRouter = createTRPCRouter({
|
|
|
53
55
|
department: departmentRouter,
|
|
54
56
|
tag: tagRouter,
|
|
55
57
|
permissions: permissionsRouter,
|
|
58
|
+
discussion: discussionRouter,
|
|
59
|
+
admin: adminRouter,
|
|
56
60
|
});
|
|
57
61
|
|
|
58
62
|
// Export type router type definition
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { RunJobStatus, RunJobType } from '@prisma/client';
|
|
3
|
+
import { createTRPCRouter, adminProcedure } from '../trpc.js';
|
|
4
|
+
import {
|
|
5
|
+
getRunJobForAdmin,
|
|
6
|
+
listRunJobsForAdmin,
|
|
7
|
+
} from '../services/adminRunJob.js';
|
|
8
|
+
import {
|
|
9
|
+
listDepartmentsForAdmin,
|
|
10
|
+
listEarlyAccessRequestsForAdmin,
|
|
11
|
+
listSchoolDevelopmentProgramsForAdmin,
|
|
12
|
+
listSchoolsForAdmin,
|
|
13
|
+
} from '../services/adminInstitution.js';
|
|
14
|
+
|
|
15
|
+
export const adminRouter = createTRPCRouter({
|
|
16
|
+
listRunJobs: adminProcedure
|
|
17
|
+
.input(
|
|
18
|
+
z.object({
|
|
19
|
+
page: z.number().int().min(0).default(0),
|
|
20
|
+
pageSize: z.number().int().min(1).max(100).default(25),
|
|
21
|
+
status: z.nativeEnum(RunJobStatus).optional(),
|
|
22
|
+
type: z.nativeEnum(RunJobType).optional(),
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
.query(({ input }) => listRunJobsForAdmin(input)),
|
|
26
|
+
|
|
27
|
+
getRunJob: adminProcedure
|
|
28
|
+
.input(z.object({ id: z.string().uuid() }))
|
|
29
|
+
.query(({ input }) => getRunJobForAdmin(input.id)),
|
|
30
|
+
|
|
31
|
+
listSchools: adminProcedure
|
|
32
|
+
.input(
|
|
33
|
+
z.object({
|
|
34
|
+
page: z.number().int().min(0).default(0),
|
|
35
|
+
pageSize: z.number().int().min(1).max(100).default(25),
|
|
36
|
+
query: z.string().optional(),
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
.query(({ input }) => listSchoolsForAdmin(input)),
|
|
40
|
+
|
|
41
|
+
listDepartments: adminProcedure
|
|
42
|
+
.input(
|
|
43
|
+
z.object({
|
|
44
|
+
page: z.number().int().min(0).default(0),
|
|
45
|
+
pageSize: z.number().int().min(1).max(100).default(25),
|
|
46
|
+
schoolId: z.string().uuid().optional(),
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
49
|
+
.query(({ input }) => listDepartmentsForAdmin(input)),
|
|
50
|
+
|
|
51
|
+
listSchoolDevelopmentPrograms: adminProcedure
|
|
52
|
+
.input(
|
|
53
|
+
z.object({
|
|
54
|
+
page: z.number().int().min(0).default(0),
|
|
55
|
+
pageSize: z.number().int().min(1).max(100).default(25),
|
|
56
|
+
status: z.string().optional(),
|
|
57
|
+
query: z.string().optional(),
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
.query(({ input }) => listSchoolDevelopmentProgramsForAdmin(input)),
|
|
61
|
+
|
|
62
|
+
listEarlyAccessRequests: adminProcedure
|
|
63
|
+
.input(
|
|
64
|
+
z.object({
|
|
65
|
+
page: z.number().int().min(0).default(0),
|
|
66
|
+
pageSize: z.number().int().min(1).max(100).default(25),
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
.query(({ input }) => listEarlyAccessRequestsForAdmin(input)),
|
|
70
|
+
});
|
package/src/routers/class.ts
CHANGED
|
@@ -60,6 +60,9 @@ export const classRouter = createTRPCRouter({
|
|
|
60
60
|
subject: z.string().optional(),
|
|
61
61
|
color: z.string().optional(),
|
|
62
62
|
departmentId: z.string().nullable().optional(),
|
|
63
|
+
discussionsEnabled: z.boolean().optional(),
|
|
64
|
+
studentsCanCreateDiscussions: z.boolean().optional(),
|
|
65
|
+
studentsCanReply: z.boolean().optional(),
|
|
63
66
|
})
|
|
64
67
|
)
|
|
65
68
|
.mutation(({ input }) => {
|