@studious-lms/server 1.8.0 → 1.9.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.
Files changed (71) hide show
  1. package/dist/middleware/security.d.ts.map +1 -1
  2. package/dist/middleware/security.js +3 -3
  3. package/dist/middleware/security.js.map +1 -1
  4. package/dist/models/agenda.d.ts +2 -2
  5. package/dist/models/announcement.d.ts +2 -2
  6. package/dist/models/assignment.d.ts +33 -33
  7. package/dist/models/class.d.ts +2 -2
  8. package/dist/models/cloudStorage.d.ts +14 -1
  9. package/dist/models/cloudStorage.d.ts.map +1 -1
  10. package/dist/models/cloudStorage.js +123 -3
  11. package/dist/models/cloudStorage.js.map +1 -1
  12. package/dist/models/event.d.ts +3 -3
  13. package/dist/models/file.d.ts +27 -26
  14. package/dist/models/file.d.ts.map +1 -1
  15. package/dist/models/file.js +4 -39
  16. package/dist/models/file.js.map +1 -1
  17. package/dist/models/labChat.d.ts +6 -6
  18. package/dist/models/section.d.ts +1 -1
  19. package/dist/routers/_app.d.ts +354 -100
  20. package/dist/routers/_app.d.ts.map +1 -1
  21. package/dist/routers/_app.js +4 -2
  22. package/dist/routers/_app.js.map +1 -1
  23. package/dist/routers/agenda.d.ts +2 -2
  24. package/dist/routers/assignment.d.ts +31 -31
  25. package/dist/routers/class.d.ts +5 -5
  26. package/dist/routers/event.d.ts +2 -2
  27. package/dist/routers/file.d.ts +2 -2
  28. package/dist/routers/folder.d.ts +24 -0
  29. package/dist/routers/folder.d.ts.map +1 -1
  30. package/dist/routers/folder.js +27 -5
  31. package/dist/routers/folder.js.map +1 -1
  32. package/dist/routers/labChat.d.ts +2 -2
  33. package/dist/routers/newtonChat.d.ts +1 -1
  34. package/dist/routers/permissions.d.ts +120 -0
  35. package/dist/routers/permissions.d.ts.map +1 -0
  36. package/dist/routers/permissions.js +50 -0
  37. package/dist/routers/permissions.js.map +1 -0
  38. package/dist/routers/tag.d.ts +5 -5
  39. package/dist/services/agenda.d.ts +2 -2
  40. package/dist/services/assignment.d.ts +31 -31
  41. package/dist/services/class.d.ts +5 -5
  42. package/dist/services/class.js +3 -3
  43. package/dist/services/class.js.map +1 -1
  44. package/dist/services/event.d.ts +2 -2
  45. package/dist/services/file.d.ts +22 -5
  46. package/dist/services/file.d.ts.map +1 -1
  47. package/dist/services/file.js +81 -45
  48. package/dist/services/file.js.map +1 -1
  49. package/dist/services/folder.d.ts +97 -2
  50. package/dist/services/folder.d.ts.map +1 -1
  51. package/dist/services/folder.js +158 -87
  52. package/dist/services/folder.js.map +1 -1
  53. package/dist/services/labChat.d.ts +2 -2
  54. package/dist/services/permissions.d.ts +188 -0
  55. package/dist/services/permissions.d.ts.map +1 -0
  56. package/dist/services/permissions.js +661 -0
  57. package/dist/services/permissions.js.map +1 -0
  58. package/package.json +1 -1
  59. package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +93 -0
  60. package/prisma/schema.prisma +74 -0
  61. package/src/middleware/security.ts +1 -1
  62. package/src/models/cloudStorage.ts +134 -1
  63. package/src/models/file.ts +2 -37
  64. package/src/routers/_app.ts +2 -0
  65. package/src/routers/folder.ts +32 -2
  66. package/src/routers/permissions.ts +71 -0
  67. package/src/services/class.ts +1 -1
  68. package/src/services/file.ts +76 -47
  69. package/src/services/folder.ts +180 -91
  70. package/src/services/permissions.ts +778 -0
  71. package/tests/unit/services/permissions.test.ts +463 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.js","sources":["services/permissions.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Permissions resolver — single source of truth for \"can user U do X to target T?\"\n *\n * Sits on top of the existing scope-derived defaults (class members, school\n * members, personal owner) computed in `getScopeDefault`. Explicit `Permission`\n * rows are *additive* — they only ever grant more access, never less. There is\n * no concept of a \"deny\" rule by design; revocation = delete the row.\n *\n * Resolution order for `requireFolderAccess(user, folder, level)`:\n * 1. Scope default (class member / school member / personal owner)\n * 2. Direct grant on the folder, OR inherited grant on any ancestor\n * (walk up to scope root)\n * First match wins; otherwise FORBIDDEN.\n *\n * `requireFileAccess` resolves the file's parent folder and delegates. Files\n * inherit from their folder in v1; per-file grants are deliberately deferred\n * (forward-compatible additive migration when needed).\n */\nimport { TRPCError } from \"@trpc/server\";\nimport type {\n PermissionLevel,\n PermissionPrincipalType,\n Prisma,\n} from \"@prisma/client\";\nimport { prisma } from \"../lib/prisma.js\";\nimport {\n type CloudScope,\n resolveFolderScope,\n} from \"../models/cloudStorage.js\";\n\nexport type AccessLevel = \"READ\" | \"WRITE\";\n\n/**\n * The set of \"audiences\" a user belongs to that an explicit grant can match.\n * Generated once per request via `expandUserPrincipals` and threaded through\n * the resolver.\n */\nexport interface UserPrincipals {\n userId: string;\n tagIds: string[];\n classTeacherIds: string[];\n classStudentIds: string[];\n /** Union of teacher + student class memberships (i.e. all CLASS principals). */\n classIds: string[];\n departmentIds: string[];\n /** Active school memberships only. */\n schoolIds: string[];\n}\n\n/**\n * Build the audience set for a user. Cheap (5 queries, all id-only) but worth\n * caching per request — most permission checks in a single tRPC call hit this.\n */\nexport async function expandUserPrincipals(\n userId: string\n): Promise<UserPrincipals> {\n const [user, asTeacher, asStudent, departments, schoolMemberships] =\n await Promise.all([\n prisma.user.findUnique({\n where: { id: userId },\n select: { tags: { select: { id: true } } },\n }),\n prisma.class.findMany({\n where: { teachers: { some: { id: userId } } },\n select: { id: true },\n }),\n prisma.class.findMany({\n where: { students: { some: { id: userId } } },\n select: { id: true },\n }),\n prisma.department.findMany({\n where: { members: { some: { id: userId } } },\n select: { id: true },\n }),\n prisma.schoolMembership.findMany({\n where: { userId, status: \"ACTIVE\" },\n select: { schoolId: true },\n }),\n ]);\n\n const classTeacherIds = asTeacher.map((c) => c.id);\n const classStudentIds = asStudent.map((c) => c.id);\n return {\n userId,\n tagIds: user?.tags.map((t) => t.id) ?? [],\n classTeacherIds,\n classStudentIds,\n classIds: Array.from(new Set([...classTeacherIds, ...classStudentIds])),\n departmentIds: departments.map((d) => d.id),\n schoolIds: schoolMemberships.map((m) => m.schoolId),\n };\n}\n\n/**\n * The principal-side `where` clause that matches any explicit grant whose\n * principal applies to this user. CLASS / CLASS_TEACHERS / CLASS_STUDENTS all\n * use `classId` but with different `principalType` values, so we have to fan\n * those out into three OR branches.\n */\nfunction principalsWhere(\n principals: UserPrincipals\n): Prisma.PermissionWhereInput {\n const ors: Prisma.PermissionWhereInput[] = [\n { principalType: \"USER\", userId: principals.userId },\n ];\n if (principals.tagIds.length) {\n ors.push({ principalType: \"TAG\", tagId: { in: principals.tagIds } });\n }\n if (principals.classIds.length) {\n ors.push({\n principalType: \"CLASS\",\n classId: { in: principals.classIds },\n });\n }\n if (principals.classTeacherIds.length) {\n ors.push({\n principalType: \"CLASS_TEACHERS\",\n classId: { in: principals.classTeacherIds },\n });\n }\n if (principals.classStudentIds.length) {\n ors.push({\n principalType: \"CLASS_STUDENTS\",\n classId: { in: principals.classStudentIds },\n });\n }\n if (principals.departmentIds.length) {\n ors.push({\n principalType: \"DEPARTMENT\",\n departmentId: { in: principals.departmentIds },\n });\n }\n if (principals.schoolIds.length) {\n ors.push({\n principalType: \"SCHOOL\",\n schoolId: { in: principals.schoolIds },\n });\n }\n return { OR: ors };\n}\n\nfunction levelSatisfies(\n granted: PermissionLevel,\n required: AccessLevel\n): boolean {\n if (required === \"READ\") return granted === \"READ\" || granted === \"WRITE\";\n return granted === \"WRITE\";\n}\n\n/** Walk up to the scope root, collecting folder ids visited along the way (inclusive). */\nasync function findAncestorIds(folderId: string): Promise<string[]> {\n const ids: string[] = [];\n let cursor: string | null = folderId;\n let visits = 0;\n while (cursor && visits < 64) {\n ids.push(cursor);\n const node: { parentFolderId: string | null } | null =\n await prisma.folder.findUnique({\n where: { id: cursor },\n select: { parentFolderId: true },\n });\n if (!node) break;\n cursor = node.parentFolderId;\n visits += 1;\n }\n return ids;\n}\n\nexport { findAncestorIds };\n\n/**\n * Compute the scope default for a user — i.e. the access level the existing\n * scope-derived rules grant, *before* any explicit ACL is consulted.\n *\n * Mirrors the logic that used to live in `requireScopeAccess` in folder.ts.\n * Returns \"WRITE\" / \"READ\" / null.\n */\nexport async function getScopeDefault(\n userId: string,\n scope: CloudScope\n): Promise<AccessLevel | null> {\n switch (scope.kind) {\n case \"CLASS\": {\n const classData = await prisma.class.findFirst({\n where: { id: scope.classId },\n select: {\n id: true,\n schoolId: true,\n teachers: { where: { id: userId }, select: { id: true } },\n students: { where: { id: userId }, select: { id: true } },\n },\n });\n if (!classData) return null;\n if (classData.teachers.length > 0) return \"WRITE\";\n const isSchoolAdmin = classData.schoolId\n ? Boolean(\n await prisma.schoolMembership.findFirst({\n where: {\n schoolId: classData.schoolId,\n userId,\n role: \"ADMIN\",\n status: \"ACTIVE\",\n },\n select: { id: true },\n })\n )\n : false;\n if (isSchoolAdmin) return \"WRITE\";\n if (classData.students.length > 0) return \"READ\";\n return null;\n }\n case \"SCHOOL_STORAGE\": {\n const membership = await prisma.schoolMembership.findFirst({\n where: { schoolId: scope.schoolId, userId, status: \"ACTIVE\" },\n select: { role: true },\n });\n if (!membership) return null;\n if (membership.role === \"ADMIN\" || membership.role === \"TEACHER\")\n return \"WRITE\";\n return \"READ\";\n }\n case \"PERSONAL\": {\n return scope.userId === userId ? \"WRITE\" : null;\n }\n }\n}\n\n/**\n * Does any of the given folder ids carry a grant matching this user at >=\n * `level`? Returns the matching folder id (highest-priority for caller use)\n * or null.\n */\nasync function findGrantOnFolders(\n folderIds: string[],\n principals: UserPrincipals,\n level: AccessLevel\n): Promise<string | null> {\n if (!folderIds.length) return null;\n const grant = await prisma.permission.findFirst({\n where: {\n folderId: { in: folderIds },\n ...principalsWhere(principals),\n ...(level === \"WRITE\" ? { level: \"WRITE\" } : {}),\n },\n select: { folderId: true },\n });\n return grant?.folderId ?? null;\n}\n\nexport interface AccessResult {\n scope: CloudScope | null;\n scopeDefault: AccessLevel | null;\n /** Truthy if the resolver returned because of an explicit grant (folder or file). */\n viaGrant: boolean;\n}\n\n/**\n * Permission gate for folder-scoped operations. Throws TRPCError on denial,\n * returns the scope (and which path granted access) on success.\n *\n * `opts.mutatesSelf` (Drive-style guard): when true, the action targets the\n * folder *itself* — rename, delete, move-this-folder. Explicit grants do NOT\n * authorize these; only scope-default WRITE does. This means a teacher sharing\n * \"Math Resources\" with the parents tag can let parents read/edit children of\n * the folder, but parents cannot rename or delete the share root. Mutations\n * targeting *children* of the folder (uploads, sub-folders, child renames)\n * pass `mutatesSelf: false` and flow through the normal grant + ancestor +\n * scope-default path.\n */\nexport async function requireFolderAccess(\n userId: string,\n folderId: string,\n level: AccessLevel,\n opts: { mutatesSelf?: boolean } = {}\n): Promise<AccessResult> {\n const scope = await resolveFolderScope(folderId);\n if (!scope) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n const scopeDefault = await getScopeDefault(userId, scope);\n if (scopeDefault && levelSatisfies(scopeDefault as PermissionLevel, level)) {\n return { scope, scopeDefault, viaGrant: false };\n }\n\n // Mutations of the share root itself are reserved for scope-default WRITE.\n // Explicit grants don't authorize delete/rename/move-self even at WRITE\n // level. We've already failed the scope-default branch above, so deny.\n if (opts.mutatesSelf && level === \"WRITE\") {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message:\n \"Only the folder's owner can rename, move, or delete it. \" +\n \"You can edit its contents.\",\n });\n }\n\n const principals = await expandUserPrincipals(userId);\n const ancestors = await findAncestorIds(folderId);\n const grantFolderId = await findGrantOnFolders(ancestors, principals, level);\n if (grantFolderId) {\n return { scope, scopeDefault, viaGrant: true };\n }\n\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message:\n level === \"WRITE\"\n ? \"You don't have write access here\"\n : \"You don't have access to this folder\",\n });\n}\n\n/**\n * Per-class role check used by file-context handlers (assignment, submission,\n * annotation, announcement, class-draft attachments). Single query.\n */\nasync function userClassRole(\n userId: string,\n classId: string\n): Promise<\"TEACHER\" | \"STUDENT\" | \"ADMIN\" | null> {\n const cls = await prisma.class.findFirst({\n where: { id: classId },\n select: {\n schoolId: true,\n teachers: { where: { id: userId }, select: { id: true } },\n students: { where: { id: userId }, select: { id: true } },\n },\n });\n if (!cls) return null;\n if (cls.teachers.length) return \"TEACHER\";\n if (cls.schoolId) {\n const admin = await prisma.schoolMembership.findFirst({\n where: {\n schoolId: cls.schoolId,\n userId,\n role: \"ADMIN\",\n status: \"ACTIVE\",\n },\n select: { id: true },\n });\n if (admin) return \"ADMIN\";\n }\n if (cls.students.length) return \"STUDENT\";\n return null;\n}\n\nconst fileGrant = (level: AccessLevel): AccessResult => ({\n scope: null,\n scopeDefault: level,\n viaGrant: false,\n});\n\n/**\n * Permission gate for file-scoped operations. Dispatches on whichever context\n * relation the file carries, so every kind of file (cloud-storage, chat\n * attachment, assignment / submission / annotation / announcement / class-draft\n * attachment, school logo) gets authorized through one entry point.\n *\n * Resolution order — first match wins:\n * 1. **Owner** — `file.userId === user`. Always WRITE.\n * 2. **Folder-bound** — delegate to `requireFolderAccess` (handles scope\n * defaults + explicit grants + ancestor inheritance).\n * 3. **Conversation/message attachment** — any conversation member READs;\n * only the sender (= owner, handled in step 1) WRITEs.\n * 4. **Assignment attachment** — class teacher/admin RW, class student R.\n * 5. **Submission file** — submission's student RW (until grading workflow\n * locks it down), class teacher/admin R.\n * 6. **Annotation file** — class teacher/admin RW, submission's student R.\n * 7. **Announcement attachment** — class teacher/admin RW, class student R.\n * 8. **Class-draft attachment** — class teacher/admin RW (drafts have no\n * students yet in practice).\n * 9. **School logo** — any active school member R, school admin W.\n * 10. **Otherwise** (incl. SDP supporting docs) — owner-only fallback already\n * covered by step 1; deny.\n */\nexport async function requireFileAccess(\n userId: string,\n fileId: string,\n level: AccessLevel\n): Promise<AccessResult> {\n const file = await prisma.file.findUnique({\n where: { id: fileId },\n select: {\n id: true,\n userId: true,\n folderId: true,\n conversationId: true,\n assignmentId: true,\n classDraftId: true,\n announcementId: true,\n schoolDevelopementProgramId: true,\n assignment: { select: { classId: true } },\n submission: {\n select: { studentId: true, assignment: { select: { classId: true } } },\n },\n annotations: {\n select: { studentId: true, assignment: { select: { classId: true } } },\n },\n announcement: { select: { classId: true } },\n schools: { select: { id: true } },\n },\n });\n if (!file) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"File not found\" });\n }\n\n if (file.userId === userId) {\n return { scope: null, scopeDefault: \"WRITE\", viaGrant: false };\n }\n\n if (file.folderId) {\n return requireFolderAccess(userId, file.folderId, level);\n }\n\n if (file.conversationId) {\n const member = await prisma.conversationMember.findFirst({\n where: { conversationId: file.conversationId, userId },\n select: { id: true },\n });\n if (member) {\n // Non-owners can only read; only the sender (owner, already handled\n // above) can mutate the attachment record.\n if (level === \"READ\") return fileGrant(\"READ\");\n }\n }\n\n if (file.assignment?.classId) {\n const role = await userClassRole(userId, file.assignment.classId);\n if (role === \"TEACHER\" || role === \"ADMIN\") return fileGrant(\"WRITE\");\n if (role === \"STUDENT\" && level === \"READ\") return fileGrant(\"READ\");\n }\n\n if (file.submission?.assignment?.classId) {\n if (file.submission.studentId === userId) return fileGrant(\"WRITE\");\n const role = await userClassRole(userId, file.submission.assignment.classId);\n if ((role === \"TEACHER\" || role === \"ADMIN\") && level === \"READ\") {\n return fileGrant(\"READ\");\n }\n }\n\n if (file.annotations?.assignment?.classId) {\n const role = await userClassRole(\n userId,\n file.annotations.assignment.classId\n );\n if (role === \"TEACHER\" || role === \"ADMIN\") return fileGrant(\"WRITE\");\n if (file.annotations.studentId === userId && level === \"READ\") {\n return fileGrant(\"READ\");\n }\n }\n\n if (file.announcement?.classId) {\n const role = await userClassRole(userId, file.announcement.classId);\n if (role === \"TEACHER\" || role === \"ADMIN\") return fileGrant(\"WRITE\");\n if (role === \"STUDENT\" && level === \"READ\") return fileGrant(\"READ\");\n }\n\n if (file.classDraftId) {\n const role = await userClassRole(userId, file.classDraftId);\n if (role === \"TEACHER\" || role === \"ADMIN\") return fileGrant(\"WRITE\");\n }\n\n if (file.schools.length) {\n // Files referenced as `School.logo` — readable by any active member of\n // any school using this file, writable only by an admin of one of them.\n const schoolIds = file.schools.map((s) => s.id);\n if (level === \"WRITE\") {\n const adminMembership = await prisma.schoolMembership.findFirst({\n where: {\n schoolId: { in: schoolIds },\n userId,\n role: \"ADMIN\",\n status: \"ACTIVE\",\n },\n select: { id: true },\n });\n if (adminMembership) return fileGrant(\"WRITE\");\n } else {\n const membership = await prisma.schoolMembership.findFirst({\n where: {\n schoolId: { in: schoolIds },\n userId,\n status: \"ACTIVE\",\n },\n select: { id: true },\n });\n if (membership) return fileGrant(\"READ\");\n }\n }\n\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message:\n level === \"WRITE\"\n ? \"You don't have write access to this file\"\n : \"You don't have access to this file\",\n });\n}\n\n/**\n * Top-most-grant computation: surface every folder the user has an explicit\n * grant on whose nearest permissioned ancestor the user does *not* also hold a\n * grant on, AND whose scope default does not already grant access. That second\n * filter avoids \"Shared with me\" duplicating things you already see in your\n * normal class/school/personal scopes.\n *\n * Files are not surfaced here — file-level grants are deferred to a future\n * feature; in v1 files inherit from their folder.\n */\nexport async function listSharedWithMe(userId: string): Promise<{\n folders: { id: string; name: string; color: string | null }[];\n files: never[];\n}> {\n const principals = await expandUserPrincipals(userId);\n\n const grants = await prisma.permission.findMany({\n where: principalsWhere(principals),\n select: { id: true, folderId: true },\n });\n\n const grantedFolderIds = new Set<string>(\n grants.map((g) => g.folderId).filter((x): x is string => Boolean(x))\n );\n\n const topFolders: { id: string; name: string; color: string | null }[] = [];\n for (const folderId of grantedFolderIds) {\n const ancestors = await findAncestorIds(folderId);\n // Skip self; we want to know if any *strict* ancestor is also granted.\n const strictAncestors = ancestors.slice(1);\n const coveredByAncestor = strictAncestors.some((id) =>\n grantedFolderIds.has(id)\n );\n if (coveredByAncestor) continue;\n\n const scope = await resolveFolderScope(folderId);\n if (scope) {\n const scopeDefault = await getScopeDefault(userId, scope);\n if (scopeDefault) continue;\n }\n\n const folder = await prisma.folder.findUnique({\n where: { id: folderId },\n select: { id: true, name: true, color: true },\n });\n if (folder) topFolders.push(folder);\n }\n\n return { folders: topFolders, files: [] };\n}\n\n/* ------------------------------------------------------------------ *\n * Grant CRUD *\n * ------------------------------------------------------------------ */\n\nexport type PermissionPrincipalInput =\n | { kind: \"USER\"; userId: string }\n | { kind: \"TAG\"; tagId: string }\n | { kind: \"CLASS\"; classId: string }\n | { kind: \"CLASS_TEACHERS\"; classId: string }\n | { kind: \"CLASS_STUDENTS\"; classId: string }\n | { kind: \"DEPARTMENT\"; departmentId: string }\n | { kind: \"SCHOOL\"; schoolId: string };\n\n/** Resolve which school an explicit grant target belongs to (for cross-school checks). */\nasync function resolveTargetSchoolId(folderId: string): Promise<string | null> {\n const scope = await resolveFolderScope(folderId);\n return scopeSchoolId(scope);\n}\n\nasync function scopeSchoolId(scope: CloudScope | null): Promise<string | null> {\n if (!scope) return null;\n switch (scope.kind) {\n case \"CLASS\": {\n const c = await prisma.class.findUnique({\n where: { id: scope.classId },\n select: { schoolId: true },\n });\n return c?.schoolId ?? null;\n }\n case \"SCHOOL_STORAGE\":\n return scope.schoolId;\n case \"PERSONAL\":\n return null;\n }\n}\n\n/** The school a principal lives in (for cross-school leak prevention). */\nasync function principalSchoolId(\n principal: PermissionPrincipalInput\n): Promise<string | null> {\n switch (principal.kind) {\n case \"USER\": {\n const u = await prisma.user.findUnique({\n where: { id: principal.userId },\n select: { schoolId: true },\n });\n return u?.schoolId ?? null;\n }\n case \"TAG\": {\n const t = await prisma.tag.findUnique({\n where: { id: principal.tagId },\n select: { schoolId: true },\n });\n return t?.schoolId ?? null;\n }\n case \"CLASS\":\n case \"CLASS_TEACHERS\":\n case \"CLASS_STUDENTS\": {\n const c = await prisma.class.findUnique({\n where: { id: principal.classId },\n select: { schoolId: true },\n });\n return c?.schoolId ?? null;\n }\n case \"DEPARTMENT\": {\n const d = await prisma.department.findUnique({\n where: { id: principal.departmentId },\n select: { schoolId: true },\n });\n return d?.schoolId ?? null;\n }\n case \"SCHOOL\":\n return principal.schoolId;\n }\n}\n\nfunction principalToColumns(principal: PermissionPrincipalInput): {\n principalType: PermissionPrincipalType;\n userId: string | null;\n tagId: string | null;\n classId: string | null;\n departmentId: string | null;\n schoolId: string | null;\n} {\n const base = {\n userId: null as string | null,\n tagId: null as string | null,\n classId: null as string | null,\n departmentId: null as string | null,\n schoolId: null as string | null,\n };\n switch (principal.kind) {\n case \"USER\":\n return { ...base, principalType: \"USER\", userId: principal.userId };\n case \"TAG\":\n return { ...base, principalType: \"TAG\", tagId: principal.tagId };\n case \"CLASS\":\n return { ...base, principalType: \"CLASS\", classId: principal.classId };\n case \"CLASS_TEACHERS\":\n return {\n ...base,\n principalType: \"CLASS_TEACHERS\",\n classId: principal.classId,\n };\n case \"CLASS_STUDENTS\":\n return {\n ...base,\n principalType: \"CLASS_STUDENTS\",\n classId: principal.classId,\n };\n case \"DEPARTMENT\":\n return {\n ...base,\n principalType: \"DEPARTMENT\",\n departmentId: principal.departmentId,\n };\n case \"SCHOOL\":\n return { ...base, principalType: \"SCHOOL\", schoolId: principal.schoolId };\n }\n}\n\n/**\n * Create a new explicit grant on a folder. Validates:\n * - Granter has WRITE on the folder.\n * - The principal is in the same school as the folder's scope (no\n * cross-school leaks). Personal-scope folders cannot be granted to\n * non-USER principals — there's no \"school\" to check membership against.\n */\nexport async function grantPermission(\n grantedById: string,\n input: {\n folderId: string;\n principal: PermissionPrincipalInput;\n level: AccessLevel;\n }\n) {\n await requireFolderAccess(grantedById, input.folderId, \"WRITE\");\n\n const [targetSchoolId, principalSchool] = await Promise.all([\n resolveTargetSchoolId(input.folderId),\n principalSchoolId(input.principal),\n ]);\n\n if (input.principal.kind !== \"USER\") {\n if (!targetSchoolId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message:\n \"Personal-scope items can only be shared with specific users, not groups\",\n });\n }\n if (principalSchool !== targetSchoolId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot share with a principal from a different school\",\n });\n }\n } else if (targetSchoolId && principalSchool && principalSchool !== targetSchoolId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot share with a user from a different school\",\n });\n }\n\n // Re-granting the same (folder, principal) pair updates the existing row's\n // level instead of throwing P2002 against the @@unique. Prisma's composite-\n // unique-key types refuse nullable columns, so we go through findFirst +\n // update/create rather than a single upsert.\n const principalCols = principalToColumns(input.principal);\n const existing = await prisma.permission.findFirst({\n where: {\n folderId: input.folderId,\n principalType: principalCols.principalType,\n userId: principalCols.userId,\n tagId: principalCols.tagId,\n classId: principalCols.classId,\n departmentId: principalCols.departmentId,\n schoolId: principalCols.schoolId,\n },\n select: { id: true },\n });\n if (existing) {\n return prisma.permission.update({\n where: { id: existing.id },\n data: { level: input.level, grantedById },\n });\n }\n return prisma.permission.create({\n data: {\n folderId: input.folderId,\n ...principalCols,\n level: input.level,\n grantedById,\n },\n });\n}\n\n/** Revoke a grant. Allowed for the granter or anyone with WRITE on the folder. */\nexport async function revokePermission(userId: string, permissionId: string) {\n const grant = await prisma.permission.findUnique({\n where: { id: permissionId },\n select: { id: true, grantedById: true, folderId: true },\n });\n if (!grant) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Permission not found\" });\n }\n if (grant.grantedById !== userId) {\n await requireFolderAccess(userId, grant.folderId, \"WRITE\");\n }\n await prisma.permission.delete({ where: { id: permissionId } });\n return { success: true };\n}\n\n/** List all explicit grants on a folder. Caller must have WRITE on it. */\nexport async function listPermissions(userId: string, folderId: string) {\n await requireFolderAccess(userId, folderId, \"WRITE\");\n return prisma.permission.findMany({\n where: { folderId },\n include: {\n user: { select: { id: true, username: true, email: true } },\n tag: { select: { id: true, name: true, color: true } },\n class: { select: { id: true, name: true, section: true } },\n department: { select: { id: true, name: true, color: true } },\n school: { select: { id: true, name: true } },\n },\n orderBy: { createdAt: \"desc\" },\n });\n}\n"],"names":[],"mappings":"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","debug_id":"2ac7304e-3502-58ed-af8e-db3b64af3009"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studious-lms/server",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Backend server for Studious application",
5
5
  "main": "dist/exportType.js",
6
6
  "types": "dist/exportType.d.ts",
@@ -0,0 +1,93 @@
1
+ -- Add folder-level ACL permissions (v1: folder-only).
2
+ --
3
+ -- This migration introduces the Permission table and its supporting enums.
4
+ -- It is *purely additive* — no existing column or row is modified, so it can
5
+ -- be applied safely on a live database without backfill.
6
+ --
7
+ -- File-level grants are deliberately deferred. When/if added later, they can
8
+ -- be a non-breaking additive migration (new columns + back-relation).
9
+
10
+ -- CreateEnum
11
+ CREATE TYPE "PermissionPrincipalType" AS ENUM (
12
+ 'USER',
13
+ 'TAG',
14
+ 'CLASS',
15
+ 'CLASS_TEACHERS',
16
+ 'CLASS_STUDENTS',
17
+ 'DEPARTMENT',
18
+ 'SCHOOL'
19
+ );
20
+
21
+ -- CreateEnum
22
+ CREATE TYPE "PermissionLevel" AS ENUM ('READ', 'WRITE');
23
+
24
+ -- CreateTable
25
+ CREATE TABLE "Permission" (
26
+ "id" TEXT NOT NULL DEFAULT gen_random_uuid()::text,
27
+ "folderId" TEXT NOT NULL,
28
+ "principalType" "PermissionPrincipalType" NOT NULL,
29
+ "userId" TEXT,
30
+ "tagId" TEXT,
31
+ "classId" TEXT,
32
+ "departmentId" TEXT,
33
+ "schoolId" TEXT,
34
+ "level" "PermissionLevel" NOT NULL,
35
+ "grantedById" TEXT NOT NULL,
36
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
37
+
38
+ CONSTRAINT "Permission_pkey" PRIMARY KEY ("id")
39
+ );
40
+
41
+ -- CreateIndex
42
+ CREATE INDEX "Permission_folderId_idx" ON "Permission"("folderId");
43
+
44
+ -- CreateIndex
45
+ CREATE INDEX "Permission_userId_idx" ON "Permission"("userId");
46
+
47
+ -- CreateIndex
48
+ CREATE INDEX "Permission_tagId_idx" ON "Permission"("tagId");
49
+
50
+ -- CreateIndex
51
+ CREATE INDEX "Permission_classId_idx" ON "Permission"("classId");
52
+
53
+ -- CreateIndex
54
+ CREATE INDEX "Permission_departmentId_idx" ON "Permission"("departmentId");
55
+
56
+ -- CreateIndex
57
+ CREATE INDEX "Permission_schoolId_idx" ON "Permission"("schoolId");
58
+
59
+ -- CreateIndex
60
+ -- One grant per (folder, principal). Re-granting upserts the level.
61
+ -- Postgres treats nulls as distinct in unique indexes, but the service layer
62
+ -- guarantees exactly one principal id is non-null per row, so this still
63
+ -- uniquely identifies the row.
64
+ CREATE UNIQUE INDEX "Permission_folderId_principalType_userId_tagId_classId_depa_key"
65
+ ON "Permission"("folderId", "principalType", "userId", "tagId", "classId", "departmentId", "schoolId");
66
+
67
+ -- AddForeignKey
68
+ ALTER TABLE "Permission" ADD CONSTRAINT "Permission_folderId_fkey"
69
+ FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
70
+
71
+ -- AddForeignKey
72
+ ALTER TABLE "Permission" ADD CONSTRAINT "Permission_userId_fkey"
73
+ FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
74
+
75
+ -- AddForeignKey
76
+ ALTER TABLE "Permission" ADD CONSTRAINT "Permission_tagId_fkey"
77
+ FOREIGN KEY ("tagId") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
78
+
79
+ -- AddForeignKey
80
+ ALTER TABLE "Permission" ADD CONSTRAINT "Permission_classId_fkey"
81
+ FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE CASCADE ON UPDATE CASCADE;
82
+
83
+ -- AddForeignKey
84
+ ALTER TABLE "Permission" ADD CONSTRAINT "Permission_departmentId_fkey"
85
+ FOREIGN KEY ("departmentId") REFERENCES "Department"("id") ON DELETE CASCADE ON UPDATE CASCADE;
86
+
87
+ -- AddForeignKey
88
+ ALTER TABLE "Permission" ADD CONSTRAINT "Permission_schoolId_fkey"
89
+ FOREIGN KEY ("schoolId") REFERENCES "School"("id") ON DELETE CASCADE ON UPDATE CASCADE;
90
+
91
+ -- AddForeignKey
92
+ ALTER TABLE "Permission" ADD CONSTRAINT "Permission_grantedById_fkey"
93
+ FOREIGN KEY ("grantedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -127,6 +127,7 @@ model School {
127
127
  departments Department[]
128
128
  tags Tag[]
129
129
  storageRoot Folder? @relation("SchoolStorageRoot")
130
+ permissions Permission[] @relation("SchoolPermissions")
130
131
  }
131
132
 
132
133
  model Department {
@@ -144,6 +145,7 @@ model Department {
144
145
  tags Tag[]
145
146
  rubricTemplates MarkScheme[]
146
147
  boundaryTemplates GradingBoundary[]
148
+ permissions Permission[] @relation("DepartmentPermissions")
147
149
 
148
150
  @@unique([schoolId, name])
149
151
  @@index([schoolId])
@@ -171,6 +173,7 @@ model Tag {
171
173
  classes Class[] @relation("ClassTags")
172
174
  rubrics MarkScheme[] @relation("MarkSchemeTags")
173
175
  boundaries GradingBoundary[] @relation("GradingBoundaryTags")
176
+ permissions Permission[] @relation("TagPermissions")
174
177
 
175
178
  @@unique([schoolId, name])
176
179
  @@index([schoolId])
@@ -213,6 +216,10 @@ model User {
213
216
  tags Tag[] @relation("UserTags")
214
217
  personalStorage Folder? @relation("UserStorageRoot")
215
218
 
219
+ // Permission grants where this user is the principal vs. the granter.
220
+ userPermissions Permission[] @relation("UserPermissions")
221
+ grantedPermissions Permission[] @relation("GrantedPermissions")
222
+
216
223
  // Chat relations
217
224
  conversationMemberships ConversationMember[]
218
225
  sentMessages Message[] @relation("SentMessages")
@@ -303,6 +310,7 @@ model Class {
303
310
  department Department? @relation(fields: [departmentId], references: [id], onDelete: SetNull)
304
311
  departmentId String?
305
312
  tags Tag[] @relation("ClassTags")
313
+ permissions Permission[] @relation("ClassPermissions")
306
314
 
307
315
  @@index([schoolId])
308
316
  @@index([departmentId])
@@ -361,6 +369,8 @@ model Folder {
361
369
  schoolStorageId String? @unique
362
370
  personalOwner User? @relation("UserStorageRoot", fields: [personalOwnerId], references: [id], onDelete: Cascade)
363
371
  personalOwnerId String? @unique
372
+
373
+ permissions Permission[] @relation("FolderPermissions")
364
374
  }
365
375
 
366
376
  model File {
@@ -898,3 +908,67 @@ model RunJob {
898
908
  @@index([classId, status])
899
909
  @@index([status, createdAt])
900
910
  }
911
+
912
+ /// What kind of principal an explicit Permission row grants access to.
913
+ /// CLASS = everyone in the class (teachers + students). CLASS_TEACHERS /
914
+ /// CLASS_STUDENTS narrow that to one role. Stored alongside a single FK
915
+ /// (`classId`, `userId`, etc.) — see service-layer invariants.
916
+ enum PermissionPrincipalType {
917
+ USER
918
+ TAG
919
+ CLASS
920
+ CLASS_TEACHERS
921
+ CLASS_STUDENTS
922
+ DEPARTMENT
923
+ SCHOOL
924
+ }
925
+
926
+ enum PermissionLevel {
927
+ READ
928
+ WRITE
929
+ }
930
+
931
+ /// Explicit ACL grant on a folder. Sits on top of (and is purely additive
932
+ /// to) the scope-derived defaults computed from FolderScope. Files inherit
933
+ /// from their parent folder — file-level overrides are deliberately deferred
934
+ /// to a future, additive migration that adds a `fileId` column + discriminator.
935
+ ///
936
+ /// Schema invariant enforced in the service layer (Prisma can't express it):
937
+ /// - Exactly one of `userId` / `tagId` / `classId` / `departmentId` /
938
+ /// `schoolId` is set, matching `principalType`. CLASS, CLASS_TEACHERS,
939
+ /// CLASS_STUDENTS all share `classId`.
940
+ model Permission {
941
+ id String @id @default(uuid())
942
+ folderId String
943
+ folder Folder @relation("FolderPermissions", fields: [folderId], references: [id], onDelete: Cascade)
944
+
945
+ principalType PermissionPrincipalType
946
+ userId String?
947
+ tagId String?
948
+ classId String?
949
+ departmentId String?
950
+ schoolId String?
951
+
952
+ user User? @relation("UserPermissions", fields: [userId], references: [id], onDelete: Cascade)
953
+ tag Tag? @relation("TagPermissions", fields: [tagId], references: [id], onDelete: Cascade)
954
+ class Class? @relation("ClassPermissions", fields: [classId], references: [id], onDelete: Cascade)
955
+ department Department? @relation("DepartmentPermissions", fields: [departmentId], references: [id], onDelete: Cascade)
956
+ school School? @relation("SchoolPermissions", fields: [schoolId], references: [id], onDelete: Cascade)
957
+
958
+ level PermissionLevel
959
+ grantedById String
960
+ grantedBy User @relation("GrantedPermissions", fields: [grantedById], references: [id])
961
+ createdAt DateTime @default(now())
962
+
963
+ /// One grant per (folder, principal). Re-granting upserts the level.
964
+ /// Postgres treats nulls as distinct in unique indexes, but the service layer
965
+ /// guarantees exactly one principal id is non-null, so this still uniquely
966
+ /// identifies the row.
967
+ @@unique([folderId, principalType, userId, tagId, classId, departmentId, schoolId])
968
+ @@index([folderId])
969
+ @@index([userId])
970
+ @@index([tagId])
971
+ @@index([classId])
972
+ @@index([departmentId])
973
+ @@index([schoolId])
974
+ }
@@ -20,7 +20,7 @@ const rateLimitHandler = (req: Request, res: Response) => {
20
20
  // General API rate limiter - applies to all routes
21
21
  export const generalLimiter = rateLimit({
22
22
  windowMs: 10 * 60 * 1000, // 10 minutes
23
- max: 100, // Limit each IP to 100 requests per windowMs
23
+ max: 1000, // Limit each IP to 100 requests per windowMs
24
24
  message: 'Too many requests from this IP, please try again later.',
25
25
  standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
26
26
  legacyHeaders: false, // Disable the `X-RateLimit-*` headers
@@ -176,10 +176,130 @@ export function renameFolder(folderId: string, name: string, color?: string) {
176
176
  });
177
177
  }
178
178
 
179
- export async function listFolderBreadcrumbs(folderId: string) {
179
+ /**
180
+ * Build the breadcrumb chain for a folder.
181
+ *
182
+ * Without `viewerId`: walks all the way up to the scope root (owner-style
183
+ * view).
184
+ *
185
+ * With `viewerId`: walks up but stops at the highest folder the viewer is
186
+ * actually granted on, OR at the scope root if the viewer is a scope member.
187
+ * This is the Drive-style "Shared with me" behavior — a grantee never sees
188
+ * ancestor folders they weren't shared on. The result for a grantee browsing
189
+ * `Owner/PrivateA/SharedB/C` (granted on B) reads `SharedB / C` — the ancestor
190
+ * `PrivateA` is correctly hidden.
191
+ */
192
+ export async function listFolderBreadcrumbs(
193
+ folderId: string,
194
+ viewerId?: string
195
+ ) {
180
196
  const breadcrumbs: { id: string; name: string }[] = [];
181
197
  let currentId: string | null = folderId;
182
198
  let visits = 0;
199
+
200
+ let viewerGrantedFolderIds: Set<string> | null = null;
201
+ let viewerScopeMember = false;
202
+
203
+ if (viewerId) {
204
+ const grants = await prisma.permission.findMany({
205
+ where: {
206
+ OR: [
207
+ { principalType: "USER", userId: viewerId },
208
+ {
209
+ principalType: "TAG",
210
+ tag: { users: { some: { id: viewerId } } },
211
+ },
212
+ {
213
+ principalType: "CLASS",
214
+ class: {
215
+ OR: [
216
+ { teachers: { some: { id: viewerId } } },
217
+ { students: { some: { id: viewerId } } },
218
+ ],
219
+ },
220
+ },
221
+ {
222
+ principalType: "CLASS_TEACHERS",
223
+ class: { teachers: { some: { id: viewerId } } },
224
+ },
225
+ {
226
+ principalType: "CLASS_STUDENTS",
227
+ class: { students: { some: { id: viewerId } } },
228
+ },
229
+ {
230
+ principalType: "DEPARTMENT",
231
+ department: { members: { some: { id: viewerId } } },
232
+ },
233
+ {
234
+ principalType: "SCHOOL",
235
+ school: {
236
+ memberships: { some: { userId: viewerId, status: "ACTIVE" } },
237
+ },
238
+ },
239
+ ],
240
+ },
241
+ select: { folderId: true },
242
+ });
243
+ viewerGrantedFolderIds = new Set(
244
+ grants.map((g) => g.folderId).filter((x): x is string => Boolean(x))
245
+ );
246
+
247
+ // Determine if the viewer is a member of the *target* folder's scope —
248
+ // if so, breadcrumbs go all the way to the scope root as normal.
249
+ const scope = await resolveFolderScope(folderId);
250
+ if (scope) {
251
+ switch (scope.kind) {
252
+ case "CLASS": {
253
+ const c = await prisma.class.findFirst({
254
+ where: {
255
+ id: scope.classId,
256
+ OR: [
257
+ { teachers: { some: { id: viewerId } } },
258
+ { students: { some: { id: viewerId } } },
259
+ ],
260
+ },
261
+ select: { id: true },
262
+ });
263
+ viewerScopeMember = Boolean(c);
264
+ if (!viewerScopeMember && scope.classId) {
265
+ const klass = await prisma.class.findUnique({
266
+ where: { id: scope.classId },
267
+ select: { schoolId: true },
268
+ });
269
+ if (klass?.schoolId) {
270
+ const m = await prisma.schoolMembership.findFirst({
271
+ where: {
272
+ schoolId: klass.schoolId,
273
+ userId: viewerId,
274
+ role: "ADMIN",
275
+ status: "ACTIVE",
276
+ },
277
+ select: { id: true },
278
+ });
279
+ viewerScopeMember = Boolean(m);
280
+ }
281
+ }
282
+ break;
283
+ }
284
+ case "SCHOOL_STORAGE": {
285
+ const m = await prisma.schoolMembership.findFirst({
286
+ where: {
287
+ schoolId: scope.schoolId,
288
+ userId: viewerId,
289
+ status: "ACTIVE",
290
+ },
291
+ select: { id: true },
292
+ });
293
+ viewerScopeMember = Boolean(m);
294
+ break;
295
+ }
296
+ case "PERSONAL":
297
+ viewerScopeMember = scope.userId === viewerId;
298
+ break;
299
+ }
300
+ }
301
+ }
302
+
183
303
  while (currentId && visits < 64) {
184
304
  const folder = await prisma.folder.findUnique({
185
305
  where: { id: currentId },
@@ -187,6 +307,19 @@ export async function listFolderBreadcrumbs(folderId: string) {
187
307
  });
188
308
  if (!folder) break;
189
309
  breadcrumbs.unshift({ id: folder.id, name: folder.name });
310
+
311
+ // Viewer-aware truncation: when the viewer is *not* a scope member but
312
+ // is a grantee, stop at the highest granted ancestor (we walk up one
313
+ // more step into the granted folder, then stop there).
314
+ if (
315
+ viewerId &&
316
+ !viewerScopeMember &&
317
+ viewerGrantedFolderIds &&
318
+ viewerGrantedFolderIds.has(folder.id)
319
+ ) {
320
+ break;
321
+ }
322
+
190
323
  currentId = folder.parentFolderId;
191
324
  visits += 1;
192
325
  }
@@ -8,9 +8,6 @@ const classFileAccessInclude = {
8
8
  teachers: true,
9
9
  school: {
10
10
  include: {
11
- users: {
12
- select: { id: true },
13
- },
14
11
  memberships: {
15
12
  where: {
16
13
  role: "ADMIN" as const,
@@ -40,23 +37,7 @@ export function findFileById(fileId: string) {
40
37
  assignment: {
41
38
  include: {
42
39
  class: {
43
- include: {
44
- teachers: true,
45
- school: {
46
- include: {
47
- users: {
48
- select: { id: true },
49
- },
50
- memberships: {
51
- where: {
52
- role: "ADMIN" as const,
53
- status: "ACTIVE" as const,
54
- },
55
- select: { userId: true },
56
- },
57
- },
58
- },
59
- },
40
+ include: classFileAccessInclude,
60
41
  },
61
42
  },
62
43
  },
@@ -68,23 +49,7 @@ export function findFileById(fileId: string) {
68
49
  assignment: {
69
50
  include: {
70
51
  class: {
71
- include: {
72
- teachers: true,
73
- school: {
74
- include: {
75
- users: {
76
- select: { id: true },
77
- },
78
- memberships: {
79
- where: {
80
- role: "ADMIN" as const,
81
- status: "ACTIVE" as const,
82
- },
83
- select: { userId: true },
84
- },
85
- },
86
- },
87
- },
52
+ include: classFileAccessInclude,
88
53
  },
89
54
  },
90
55
  },
@@ -25,6 +25,7 @@ import { searchRouter } from "./search.js";
25
25
  import { schoolRouter } from "./school.js";
26
26
  import { departmentRouter } from "./department.js";
27
27
  import { tagRouter } from "./tag.js";
28
+ import { permissionsRouter } from "./permissions.js";
28
29
 
29
30
  export const appRouter = createTRPCRouter({
30
31
  class: classRouter,
@@ -51,6 +52,7 @@ export const appRouter = createTRPCRouter({
51
52
  school: schoolRouter,
52
53
  department: departmentRouter,
53
54
  tag: tagRouter,
55
+ permissions: permissionsRouter,
54
56
  });
55
57
 
56
58
  // Export type router type definition
@@ -5,6 +5,7 @@ import {
5
5
  } from "../trpc.js";
6
6
  import {
7
7
  browseFolder,
8
+ browseSharedWithMe,
8
9
  confirmFolderUpload,
9
10
  createFolderRecord,
10
11
  deleteFolder,
@@ -16,6 +17,7 @@ import {
16
17
  moveFolder,
17
18
  updateFolderRecord,
18
19
  } from "../services/folder.js";
20
+ import { listSharedWithMe } from "../services/permissions.js";
19
21
 
20
22
  const directFileSchema = z.object({
21
23
  name: z.string(),
@@ -40,16 +42,44 @@ const cloudScopeSchema = z.discriminatedUnion("kind", [
40
42
  z.object({ kind: z.literal("PERSONAL"), userId: z.string() }),
41
43
  ]);
42
44
 
45
+ /**
46
+ * Extended scope schema for `browse` only. SHARED_WITH_ME is a *virtual*
47
+ * scope — it has no root row in the `Folder` table and you can't create
48
+ * folders inside it directly. It just collects every top-most explicit grant
49
+ * the user holds for "Shared with me"-style display.
50
+ */
51
+ const browseScopeSchema = z.discriminatedUnion("kind", [
52
+ z.object({ kind: z.literal("CLASS"), classId: z.string() }),
53
+ z.object({ kind: z.literal("SCHOOL_STORAGE"), schoolId: z.string() }),
54
+ z.object({ kind: z.literal("PERSONAL"), userId: z.string() }),
55
+ z.object({ kind: z.literal("SHARED_WITH_ME") }),
56
+ ]);
57
+
43
58
  export const folderRouter = createTRPCRouter({
44
59
  /** Browse a folder (or scope root) with breadcrumbs and access flag. */
45
60
  browse: protectedProcedure
46
61
  .input(
47
62
  z.union([
48
63
  z.object({ folderId: z.string() }),
49
- z.object({ scope: cloudScopeSchema }),
64
+ z.object({ scope: browseScopeSchema }),
50
65
  ])
51
66
  )
52
- .query(({ ctx, input }) => browseFolder(ctx.user!.id, input)),
67
+ .query(({ ctx, input }) => {
68
+ if ("scope" in input && input.scope.kind === "SHARED_WITH_ME") {
69
+ return browseSharedWithMe(ctx.user!.id);
70
+ }
71
+ return browseFolder(
72
+ ctx.user!.id,
73
+ "scope" in input
74
+ ? { scope: input.scope as Exclude<typeof input.scope, { kind: "SHARED_WITH_ME" }> }
75
+ : input
76
+ );
77
+ }),
78
+
79
+ /** Top-most folders/files explicitly shared with the caller (Drive-style). */
80
+ listSharedWithMe: protectedProcedure.query(({ ctx }) =>
81
+ listSharedWithMe(ctx.user!.id)
82
+ ),
53
83
 
54
84
  /** What scopes can this user pick from in the cloud-storage UI? */
55
85
  listScopes: protectedProcedure.query(({ ctx }) =>