@studious-lms/server 1.7.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 (212) 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/access.d.ts +21 -66
  5. package/dist/models/access.d.ts.map +1 -1
  6. package/dist/models/access.js +10 -26
  7. package/dist/models/access.js.map +1 -1
  8. package/dist/models/agenda.d.ts +4 -2
  9. package/dist/models/agenda.d.ts.map +1 -1
  10. package/dist/models/announcement.d.ts +4 -3
  11. package/dist/models/announcement.d.ts.map +1 -1
  12. package/dist/models/announcement.js +2 -3
  13. package/dist/models/announcement.js.map +1 -1
  14. package/dist/models/assignment.d.ts +40 -33
  15. package/dist/models/assignment.d.ts.map +1 -1
  16. package/dist/models/auth.d.ts +2 -4
  17. package/dist/models/auth.d.ts.map +1 -1
  18. package/dist/models/auth.js +2 -3
  19. package/dist/models/auth.js.map +1 -1
  20. package/dist/models/class.d.ts +34 -6
  21. package/dist/models/class.d.ts.map +1 -1
  22. package/dist/models/class.js +15 -2
  23. package/dist/models/class.js.map +1 -1
  24. package/dist/models/cloudStorage.d.ts +153 -0
  25. package/dist/models/cloudStorage.d.ts.map +1 -0
  26. package/dist/models/cloudStorage.js +295 -0
  27. package/dist/models/cloudStorage.js.map +1 -0
  28. package/dist/models/comment.d.ts +0 -24
  29. package/dist/models/comment.d.ts.map +1 -1
  30. package/dist/models/comment.js +2 -6
  31. package/dist/models/comment.js.map +1 -1
  32. package/dist/models/conversation.d.ts +22 -22
  33. package/dist/models/department.d.ts +154 -0
  34. package/dist/models/department.d.ts.map +1 -0
  35. package/dist/models/department.js +135 -0
  36. package/dist/models/department.js.map +1 -0
  37. package/dist/models/event.d.ts +10 -4
  38. package/dist/models/event.d.ts.map +1 -1
  39. package/dist/models/event.js +2 -5
  40. package/dist/models/event.js.map +1 -1
  41. package/dist/models/file.d.ts +52 -36
  42. package/dist/models/file.d.ts.map +1 -1
  43. package/dist/models/file.js +4 -42
  44. package/dist/models/file.js.map +1 -1
  45. package/dist/models/folder.d.ts +47 -0
  46. package/dist/models/folder.d.ts.map +1 -1
  47. package/dist/models/labChat.d.ts +11 -9
  48. package/dist/models/labChat.d.ts.map +1 -1
  49. package/dist/models/message.d.ts +46 -12
  50. package/dist/models/message.d.ts.map +1 -1
  51. package/dist/models/message.js +19 -10
  52. package/dist/models/message.js.map +1 -1
  53. package/dist/models/newtonChat.d.ts +2 -1
  54. package/dist/models/newtonChat.d.ts.map +1 -1
  55. package/dist/models/school.d.ts +45 -12
  56. package/dist/models/school.d.ts.map +1 -1
  57. package/dist/models/school.js +19 -2
  58. package/dist/models/school.js.map +1 -1
  59. package/dist/models/section.d.ts +2 -1
  60. package/dist/models/section.d.ts.map +1 -1
  61. package/dist/models/tag.d.ts +117 -0
  62. package/dist/models/tag.d.ts.map +1 -0
  63. package/dist/models/tag.js +113 -0
  64. package/dist/models/tag.js.map +1 -0
  65. package/dist/models/worksheet.d.ts +3 -0
  66. package/dist/models/worksheet.d.ts.map +1 -1
  67. package/dist/pipelines/aiLabChatContract.d.ts +8 -8
  68. package/dist/routers/_app.d.ts +6704 -5222
  69. package/dist/routers/_app.d.ts.map +1 -1
  70. package/dist/routers/_app.js +8 -2
  71. package/dist/routers/_app.js.map +1 -1
  72. package/dist/routers/agenda.d.ts +4 -2
  73. package/dist/routers/agenda.d.ts.map +1 -1
  74. package/dist/routers/announcement.d.ts.map +1 -1
  75. package/dist/routers/announcement.js +3 -5
  76. package/dist/routers/announcement.js.map +1 -1
  77. package/dist/routers/assignment.d.ts +49 -40
  78. package/dist/routers/assignment.d.ts.map +1 -1
  79. package/dist/routers/assignment.js +2 -3
  80. package/dist/routers/assignment.js.map +1 -1
  81. package/dist/routers/class.d.ts +92 -19
  82. package/dist/routers/class.d.ts.map +1 -1
  83. package/dist/routers/class.js +5 -2
  84. package/dist/routers/class.js.map +1 -1
  85. package/dist/routers/conversation.d.ts +5 -5
  86. package/dist/routers/department.d.ts +293 -0
  87. package/dist/routers/department.d.ts.map +1 -0
  88. package/dist/routers/department.js +93 -0
  89. package/dist/routers/department.js.map +1 -0
  90. package/dist/routers/event.d.ts +4 -3
  91. package/dist/routers/event.d.ts.map +1 -1
  92. package/dist/routers/file.d.ts +10 -8
  93. package/dist/routers/file.d.ts.map +1 -1
  94. package/dist/routers/file.js +26 -17
  95. package/dist/routers/file.js.map +1 -1
  96. package/dist/routers/folder.d.ts +163 -186
  97. package/dist/routers/folder.d.ts.map +1 -1
  98. package/dist/routers/folder.js +102 -88
  99. package/dist/routers/folder.js.map +1 -1
  100. package/dist/routers/labChat.d.ts +13 -4
  101. package/dist/routers/labChat.d.ts.map +1 -1
  102. package/dist/routers/message.d.ts +20 -2
  103. package/dist/routers/message.d.ts.map +1 -1
  104. package/dist/routers/newtonChat.d.ts +10 -1
  105. package/dist/routers/newtonChat.d.ts.map +1 -1
  106. package/dist/routers/permissions.d.ts +120 -0
  107. package/dist/routers/permissions.d.ts.map +1 -0
  108. package/dist/routers/permissions.js +50 -0
  109. package/dist/routers/permissions.js.map +1 -0
  110. package/dist/routers/school.d.ts +160 -17
  111. package/dist/routers/school.d.ts.map +1 -1
  112. package/dist/routers/school.js +46 -3
  113. package/dist/routers/school.js.map +1 -1
  114. package/dist/routers/tag.d.ts +119 -0
  115. package/dist/routers/tag.d.ts.map +1 -0
  116. package/dist/routers/tag.js +52 -0
  117. package/dist/routers/tag.js.map +1 -0
  118. package/dist/routers/worksheet.d.ts +1 -0
  119. package/dist/routers/worksheet.d.ts.map +1 -1
  120. package/dist/seedDatabase.d.ts +0 -1
  121. package/dist/seedDatabase.d.ts.map +1 -1
  122. package/dist/services/agenda.d.ts +4 -2
  123. package/dist/services/agenda.d.ts.map +1 -1
  124. package/dist/services/assignment.d.ts +49 -40
  125. package/dist/services/assignment.d.ts.map +1 -1
  126. package/dist/services/class.d.ts +92 -19
  127. package/dist/services/class.d.ts.map +1 -1
  128. package/dist/services/class.js +92 -5
  129. package/dist/services/class.js.map +1 -1
  130. package/dist/services/conversation.d.ts +5 -5
  131. package/dist/services/department.d.ts +200 -0
  132. package/dist/services/department.d.ts.map +1 -0
  133. package/dist/services/department.js +368 -0
  134. package/dist/services/department.js.map +1 -0
  135. package/dist/services/event.d.ts +4 -3
  136. package/dist/services/event.d.ts.map +1 -1
  137. package/dist/services/file.d.ts +105 -3
  138. package/dist/services/file.d.ts.map +1 -1
  139. package/dist/services/file.js +167 -41
  140. package/dist/services/file.js.map +1 -1
  141. package/dist/services/folder.d.ts +220 -149
  142. package/dist/services/folder.d.ts.map +1 -1
  143. package/dist/services/folder.js +326 -196
  144. package/dist/services/folder.js.map +1 -1
  145. package/dist/services/labChat.d.ts +13 -4
  146. package/dist/services/labChat.d.ts.map +1 -1
  147. package/dist/services/labChat.js +4 -3
  148. package/dist/services/labChat.js.map +1 -1
  149. package/dist/services/message.d.ts +20 -2
  150. package/dist/services/message.d.ts.map +1 -1
  151. package/dist/services/message.js +7 -23
  152. package/dist/services/message.js.map +1 -1
  153. package/dist/services/newtonChat.d.ts +9 -0
  154. package/dist/services/newtonChat.d.ts.map +1 -1
  155. package/dist/services/newtonChat.js +4 -3
  156. package/dist/services/newtonChat.js.map +1 -1
  157. package/dist/services/permissions.d.ts +188 -0
  158. package/dist/services/permissions.d.ts.map +1 -0
  159. package/dist/services/permissions.js +661 -0
  160. package/dist/services/permissions.js.map +1 -0
  161. package/dist/services/school.d.ts +142 -15
  162. package/dist/services/school.d.ts.map +1 -1
  163. package/dist/services/school.js +308 -54
  164. package/dist/services/school.js.map +1 -1
  165. package/dist/services/tag.d.ts +71 -0
  166. package/dist/services/tag.d.ts.map +1 -0
  167. package/dist/services/tag.js +178 -0
  168. package/dist/services/tag.js.map +1 -0
  169. package/dist/services/worksheet.d.ts +1 -0
  170. package/dist/services/worksheet.d.ts.map +1 -1
  171. package/dist/utils/aiUser.d.ts.map +1 -1
  172. package/dist/utils/aiUser.js +2 -3
  173. package/dist/utils/aiUser.js.map +1 -1
  174. package/package.json +1 -1
  175. package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +184 -0
  176. package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +93 -0
  177. package/prisma/schema.prisma +193 -35
  178. package/src/middleware/security.ts +1 -1
  179. package/src/models/access.ts +8 -24
  180. package/src/models/announcement.ts +0 -1
  181. package/src/models/auth.ts +0 -1
  182. package/src/models/class.ts +13 -0
  183. package/src/models/cloudStorage.ts +327 -0
  184. package/src/models/comment.ts +0 -4
  185. package/src/models/department.ts +159 -0
  186. package/src/models/event.ts +0 -3
  187. package/src/models/file.ts +2 -40
  188. package/src/models/message.ts +19 -5
  189. package/src/models/school.ts +26 -0
  190. package/src/models/tag.ts +140 -0
  191. package/src/routers/_app.ts +6 -0
  192. package/src/routers/announcement.ts +1 -3
  193. package/src/routers/assignment.ts +0 -1
  194. package/src/routers/class.ts +3 -0
  195. package/src/routers/department.ts +174 -0
  196. package/src/routers/file.ts +29 -26
  197. package/src/routers/folder.ts +127 -118
  198. package/src/routers/permissions.ts +71 -0
  199. package/src/routers/school.ts +83 -0
  200. package/src/routers/tag.ts +78 -0
  201. package/src/services/class.ts +109 -4
  202. package/src/services/department.ts +478 -0
  203. package/src/services/file.ts +172 -43
  204. package/src/services/folder.ts +399 -237
  205. package/src/services/labChat.ts +2 -1
  206. package/src/services/message.ts +5 -21
  207. package/src/services/newtonChat.ts +2 -1
  208. package/src/services/permissions.ts +778 -0
  209. package/src/services/school.ts +369 -57
  210. package/src/services/tag.ts +229 -0
  211. package/src/utils/aiUser.ts +0 -1
  212. package/tests/unit/services/permissions.test.ts +463 -0
@@ -0,0 +1,174 @@
1
+ import { z } from "zod";
2
+ import { createTRPCRouter, protectedProcedure } from "../trpc.js";
3
+ import {
4
+ addMembersToDepartment,
5
+ applyBoundaryTemplate,
6
+ applyRubricTemplate,
7
+ createBoundaryTemplate,
8
+ createDepartmentRecord,
9
+ createRubricTemplate,
10
+ deleteBoundaryTemplate,
11
+ deleteDepartmentRecord,
12
+ deleteRubricTemplate,
13
+ getDepartmentDetails,
14
+ listDepartments,
15
+ listMyDepartments,
16
+ removeMemberFromDepartment,
17
+ setClassDepartment,
18
+ updateBoundaryTemplate,
19
+ updateDepartmentRecord,
20
+ updateRubricTemplate,
21
+ } from "../services/department.js";
22
+
23
+ export const departmentRouter = createTRPCRouter({
24
+ list: protectedProcedure
25
+ .input(z.object({ schoolId: z.string() }))
26
+ .query(({ ctx, input }) => listDepartments(ctx.user!.id, input.schoolId)),
27
+
28
+ listMine: protectedProcedure
29
+ .input(z.object({ schoolId: z.string() }))
30
+ .query(({ ctx, input }) =>
31
+ listMyDepartments(ctx.user!.id, input.schoolId)
32
+ ),
33
+
34
+ get: protectedProcedure
35
+ .input(z.object({ departmentId: z.string() }))
36
+ .query(({ ctx, input }) =>
37
+ getDepartmentDetails(ctx.user!.id, input.departmentId)
38
+ ),
39
+
40
+ create: protectedProcedure
41
+ .input(
42
+ z.object({
43
+ schoolId: z.string(),
44
+ name: z.string().min(1),
45
+ description: z.string().optional(),
46
+ color: z.string().optional(),
47
+ })
48
+ )
49
+ .mutation(({ ctx, input }) =>
50
+ createDepartmentRecord(ctx.user!.id, input)
51
+ ),
52
+
53
+ update: protectedProcedure
54
+ .input(
55
+ z.object({
56
+ departmentId: z.string(),
57
+ name: z.string().min(1).optional(),
58
+ description: z.string().nullable().optional(),
59
+ color: z.string().optional(),
60
+ })
61
+ )
62
+ .mutation(({ ctx, input }) =>
63
+ updateDepartmentRecord(ctx.user!.id, input)
64
+ ),
65
+
66
+ delete: protectedProcedure
67
+ .input(z.object({ departmentId: z.string() }))
68
+ .mutation(({ ctx, input }) =>
69
+ deleteDepartmentRecord(ctx.user!.id, input.departmentId)
70
+ ),
71
+
72
+ addMembers: protectedProcedure
73
+ .input(
74
+ z.object({
75
+ departmentId: z.string(),
76
+ userIds: z.array(z.string()),
77
+ })
78
+ )
79
+ .mutation(({ ctx, input }) =>
80
+ addMembersToDepartment(ctx.user!.id, input)
81
+ ),
82
+
83
+ removeMember: protectedProcedure
84
+ .input(z.object({ departmentId: z.string(), memberId: z.string() }))
85
+ .mutation(({ ctx, input }) =>
86
+ removeMemberFromDepartment(ctx.user!.id, input)
87
+ ),
88
+
89
+ setClassDepartment: protectedProcedure
90
+ .input(
91
+ z.object({
92
+ classId: z.string(),
93
+ departmentId: z.string().nullable(),
94
+ })
95
+ )
96
+ .mutation(({ ctx, input }) => setClassDepartment(ctx.user!.id, input)),
97
+
98
+ /** Template authoring. */
99
+ createRubricTemplate: protectedProcedure
100
+ .input(
101
+ z.object({
102
+ departmentId: z.string(),
103
+ name: z.string(),
104
+ structured: z.string(),
105
+ })
106
+ )
107
+ .mutation(({ ctx, input }) =>
108
+ createRubricTemplate(ctx.user!.id, input)
109
+ ),
110
+
111
+ updateRubricTemplate: protectedProcedure
112
+ .input(
113
+ z.object({
114
+ templateId: z.string(),
115
+ name: z.string().optional(),
116
+ structured: z.string().optional(),
117
+ })
118
+ )
119
+ .mutation(({ ctx, input }) =>
120
+ updateRubricTemplate(ctx.user!.id, input)
121
+ ),
122
+
123
+ deleteRubricTemplate: protectedProcedure
124
+ .input(z.object({ templateId: z.string() }))
125
+ .mutation(({ ctx, input }) =>
126
+ deleteRubricTemplate(ctx.user!.id, input.templateId)
127
+ ),
128
+
129
+ createBoundaryTemplate: protectedProcedure
130
+ .input(
131
+ z.object({
132
+ departmentId: z.string(),
133
+ name: z.string(),
134
+ structured: z.string(),
135
+ })
136
+ )
137
+ .mutation(({ ctx, input }) =>
138
+ createBoundaryTemplate(ctx.user!.id, input)
139
+ ),
140
+
141
+ updateBoundaryTemplate: protectedProcedure
142
+ .input(
143
+ z.object({
144
+ templateId: z.string(),
145
+ name: z.string().optional(),
146
+ structured: z.string().optional(),
147
+ })
148
+ )
149
+ .mutation(({ ctx, input }) =>
150
+ updateBoundaryTemplate(ctx.user!.id, input)
151
+ ),
152
+
153
+ deleteBoundaryTemplate: protectedProcedure
154
+ .input(z.object({ templateId: z.string() }))
155
+ .mutation(({ ctx, input }) =>
156
+ deleteBoundaryTemplate(ctx.user!.id, input.templateId)
157
+ ),
158
+
159
+ applyRubricTemplate: protectedProcedure
160
+ .input(
161
+ z.object({ classId: z.string(), templateId: z.string() })
162
+ )
163
+ .mutation(({ ctx, input }) =>
164
+ applyRubricTemplate(ctx.user!.id, input)
165
+ ),
166
+
167
+ applyBoundaryTemplate: protectedProcedure
168
+ .input(
169
+ z.object({ classId: z.string(), templateId: z.string() })
170
+ )
171
+ .mutation(({ ctx, input }) =>
172
+ applyBoundaryTemplate(ctx.user!.id, input)
173
+ ),
174
+ });
@@ -1,16 +1,20 @@
1
1
  import { z } from "zod";
2
+ import { createTRPCRouter, protectedProcedure } from "../trpc.js";
2
3
  import {
3
- createTRPCRouter,
4
- protectedProcedure,
5
- protectedTeacherProcedure,
6
- } from "../trpc.js";
7
- import {
4
+ deleteFileGeneric,
8
5
  getFileSignedUrl,
9
- moveFile,
10
- renameFile,
11
- deleteFileRecord,
6
+ moveFileScoped,
7
+ renameFileScoped,
12
8
  } from "../services/file.js";
13
9
 
10
+ /**
11
+ * File router. Backed by the same unified scope-aware permission model as the
12
+ * folder router. The class files page and the cloud-storage page (personal /
13
+ * institution) both run through the same endpoints.
14
+ *
15
+ * Permissions are derived from the file's parent folder scope, so callers
16
+ * never need to send `classId` / `scope` — just the file id.
17
+ */
14
18
  export const fileRouter = createTRPCRouter({
15
19
  getSignedUrl: protectedProcedure
16
20
  .input(z.object({ fileId: z.string() }))
@@ -18,38 +22,37 @@ export const fileRouter = createTRPCRouter({
18
22
  getFileSignedUrl(ctx.user!.id, input.fileId)
19
23
  ),
20
24
 
21
- move: protectedTeacherProcedure
25
+ move: protectedProcedure
22
26
  .input(
23
27
  z.object({
24
28
  fileId: z.string(),
25
29
  targetFolderId: z.string(),
26
- classId: z.string(),
27
30
  })
28
31
  )
29
- .mutation(({ input }) =>
30
- moveFile(input.fileId, input.targetFolderId)
32
+ .mutation(({ ctx, input }) =>
33
+ moveFileScoped(ctx.user!.id, {
34
+ fileId: input.fileId,
35
+ targetFolderId: input.targetFolderId,
36
+ })
31
37
  ),
32
38
 
33
- rename: protectedTeacherProcedure
39
+ rename: protectedProcedure
34
40
  .input(
35
41
  z.object({
36
42
  fileId: z.string(),
37
- newName: z.string(),
38
- classId: z.string(),
43
+ newName: z.string().min(1),
39
44
  })
40
45
  )
41
- .mutation(({ input }) =>
42
- renameFile(input.fileId, input.newName)
46
+ .mutation(({ ctx, input }) =>
47
+ renameFileScoped(ctx.user!.id, {
48
+ fileId: input.fileId,
49
+ newName: input.newName,
50
+ })
43
51
  ),
44
52
 
45
- delete: protectedTeacherProcedure
46
- .input(
47
- z.object({
48
- fileId: z.string(),
49
- classId: z.string(),
50
- })
51
- )
52
- .mutation(({ input }) =>
53
- deleteFileRecord(input.fileId, input.classId)
53
+ delete: protectedProcedure
54
+ .input(z.object({ fileId: z.string() }))
55
+ .mutation(({ ctx, input }) =>
56
+ deleteFileGeneric(ctx.user!.id, input.fileId)
54
57
  ),
55
58
  });
@@ -2,28 +2,22 @@ import { z } from "zod";
2
2
  import {
3
3
  createTRPCRouter,
4
4
  protectedProcedure,
5
- protectedClassMemberProcedure,
6
- protectedTeacherProcedure,
7
5
  } from "../trpc.js";
8
6
  import {
7
+ browseFolder,
8
+ browseSharedWithMe,
9
+ confirmFolderUpload,
9
10
  createFolderRecord,
11
+ deleteFolder,
10
12
  getFolder,
11
- getChildFolders,
12
- getFolderChildren,
13
+ getFolderParents,
14
+ getFolderUploadUrls,
13
15
  getRootFolder,
14
- uploadFilesToFolder,
15
- deleteFolderRecord,
16
+ listUserScopes,
16
17
  moveFolder,
17
18
  updateFolderRecord,
18
- getFolderParents,
19
- getFolderUploadUrls,
20
19
  } from "../services/folder.js";
21
-
22
- const createFolderSchema = z.object({
23
- name: z.string(),
24
- parentFolderId: z.string().optional(),
25
- color: z.string().optional(),
26
- });
20
+ import { listSharedWithMe } from "../services/permissions.js";
27
21
 
28
22
  const directFileSchema = z.object({
29
23
  name: z.string(),
@@ -31,146 +25,161 @@ const directFileSchema = z.object({
31
25
  size: z.number(),
32
26
  });
33
27
 
34
- const uploadFilesToFolderSchema = z.object({
35
- folderId: z.string(),
36
- files: z.array(directFileSchema),
37
- });
38
-
39
- const getRootFolderSchema = z.object({
40
- classId: z.string(),
41
- });
28
+ /**
29
+ * Cloud scope discriminator. Folders live under one of:
30
+ * - CLASS: a class shared-files area
31
+ * - SCHOOL_STORAGE: institution-wide files
32
+ * - PERSONAL: a single user's storage
33
+ *
34
+ * All folder operations identify the location they apply to with either a
35
+ * `folderId` (for an existing folder) or a `scope` (when no folder exists yet,
36
+ * e.g. browsing a scope root). There is no `classId` shorthand — use
37
+ * `{ scope: { kind: "CLASS", classId } }` instead.
38
+ */
39
+ const cloudScopeSchema = z.discriminatedUnion("kind", [
40
+ z.object({ kind: z.literal("CLASS"), classId: z.string() }),
41
+ z.object({ kind: z.literal("SCHOOL_STORAGE"), schoolId: z.string() }),
42
+ z.object({ kind: z.literal("PERSONAL"), userId: z.string() }),
43
+ ]);
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
+ ]);
42
57
 
43
58
  export const folderRouter = createTRPCRouter({
44
- create: protectedTeacherProcedure
45
- .input(createFolderSchema)
46
- .mutation(async ({ ctx, input }) => {
47
- if (!ctx.user) {
48
- throw new Error("You must be logged in to create a folder");
59
+ /** Browse a folder (or scope root) with breadcrumbs and access flag. */
60
+ browse: protectedProcedure
61
+ .input(
62
+ z.union([
63
+ z.object({ folderId: z.string() }),
64
+ z.object({ scope: browseScopeSchema }),
65
+ ])
66
+ )
67
+ .query(({ ctx, input }) => {
68
+ if ("scope" in input && input.scope.kind === "SHARED_WITH_ME") {
69
+ return browseSharedWithMe(ctx.user!.id);
49
70
  }
50
- return createFolderRecord(ctx.user.id, {
51
- classId: input.classId,
52
- name: input.name,
53
- parentFolderId: input.parentFolderId,
54
- color: input.color,
55
- });
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
+ );
56
77
  }),
57
78
 
58
- get: protectedClassMemberProcedure
59
- .input(z.object({ folderId: z.string(), classId: z.string() }))
60
- .query(({ input }) => getFolder(input.folderId)),
79
+ /** Top-most folders/files explicitly shared with the caller (Drive-style). */
80
+ listSharedWithMe: protectedProcedure.query(({ ctx }) =>
81
+ listSharedWithMe(ctx.user!.id)
82
+ ),
61
83
 
62
- getChildFolders: protectedClassMemberProcedure
63
- .input(z.object({ classId: z.string() }))
64
- .query(({ input }) => getChildFolders(input.classId)),
84
+ /** What scopes can this user pick from in the cloud-storage UI? */
85
+ listScopes: protectedProcedure.query(({ ctx }) =>
86
+ listUserScopes(ctx.user!.id)
87
+ ),
65
88
 
66
- getFolderChildren: protectedClassMemberProcedure
67
- .input(z.object({ folderId: z.string(), classId: z.string() }))
68
- .query(({ input }) =>
69
- getFolderChildren(input.folderId, input.classId)
70
- ),
71
-
72
- getRootFolder: protectedClassMemberProcedure
73
- .input(getRootFolderSchema)
74
- .query(({ input }) => getRootFolder(input.classId)),
89
+ /** Root folder for any scope (no folderId). */
90
+ getRootFolder: protectedProcedure
91
+ .input(z.object({ scope: cloudScopeSchema }))
92
+ .query(({ ctx, input }) => getRootFolder(ctx.user!.id, input.scope)),
75
93
 
76
- uploadFiles: protectedTeacherProcedure
77
- .input(uploadFilesToFolderSchema)
78
- .mutation(async ({ ctx, input }) => {
79
- if (!ctx.user) {
80
- throw new Error("You must be logged in to upload files");
81
- }
82
- return uploadFilesToFolder(
83
- ctx.user.id,
84
- input.classId,
85
- input.folderId,
86
- input.files
87
- );
88
- }),
94
+ get: protectedProcedure
95
+ .input(z.object({ folderId: z.string() }))
96
+ .query(({ ctx, input }) => getFolder(ctx.user!.id, input.folderId)),
89
97
 
90
- delete: protectedTeacherProcedure
91
- .input(z.object({ classId: z.string(), folderId: z.string() }))
92
- .mutation(async ({ ctx, input }) => {
93
- if (!ctx.user) {
94
- throw new Error("You must be logged in to delete folders");
95
- }
96
- return deleteFolderRecord(ctx.user.id, input.classId, input.folderId);
97
- }),
98
+ getParents: protectedProcedure
99
+ .input(z.object({ folderId: z.string() }))
100
+ .query(({ ctx, input }) => getFolderParents(ctx.user!.id, input.folderId)),
98
101
 
99
- move: protectedTeacherProcedure
102
+ create: protectedProcedure
100
103
  .input(
101
104
  z.object({
102
- folderId: z.string(),
103
- targetParentFolderId: z.string(),
104
- classId: z.string(),
105
+ name: z.string().min(1),
106
+ parentFolderId: z.string().nullable().optional(),
107
+ color: z.string().optional(),
108
+ scope: cloudScopeSchema.optional(),
105
109
  })
106
110
  )
107
- .mutation(async ({ ctx, input }) => {
108
- if (!ctx.user) {
109
- throw new Error("You must be logged in to move folders");
110
- }
111
- return moveFolder(
112
- ctx.user.id,
113
- input.folderId,
114
- input.targetParentFolderId,
115
- input.classId
116
- );
117
- }),
111
+ .mutation(({ ctx, input }) =>
112
+ createFolderRecord(ctx.user!.id, {
113
+ name: input.name,
114
+ parentFolderId: input.parentFolderId ?? undefined,
115
+ color: input.color,
116
+ scope: input.scope,
117
+ })
118
+ ),
118
119
 
119
- update: protectedTeacherProcedure
120
+ update: protectedProcedure
120
121
  .input(
121
122
  z.object({
122
123
  folderId: z.string(),
123
- name: z.string(),
124
+ name: z.string().optional(),
124
125
  color: z.string().optional(),
125
- classId: z.string(),
126
126
  })
127
127
  )
128
- .mutation(({ input }) =>
129
- updateFolderRecord(
130
- input.folderId,
131
- input.name,
132
- input.color,
133
- input.classId
134
- )
128
+ .mutation(({ ctx, input }) =>
129
+ updateFolderRecord(ctx.user!.id, {
130
+ folderId: input.folderId,
131
+ name: input.name,
132
+ color: input.color,
133
+ })
135
134
  ),
136
135
 
137
- getParents: protectedProcedure
136
+ delete: protectedProcedure
138
137
  .input(z.object({ folderId: z.string() }))
139
- .query(({ input }) => getFolderParents(input.folderId)),
138
+ .mutation(({ ctx, input }) => deleteFolder(ctx.user!.id, input.folderId)),
139
+
140
+ move: protectedProcedure
141
+ .input(
142
+ z.object({
143
+ folderId: z.string(),
144
+ targetParentFolderId: z.string(),
145
+ })
146
+ )
147
+ .mutation(({ ctx, input }) =>
148
+ moveFolder(ctx.user!.id, {
149
+ folderId: input.folderId,
150
+ targetParentFolderId: input.targetParentFolderId,
151
+ })
152
+ ),
140
153
 
141
- getFolderUploadUrls: protectedTeacherProcedure
154
+ /** Get signed upload URLs for direct-to-GCS upload. */
155
+ getFolderUploadUrls: protectedProcedure
142
156
  .input(
143
157
  z.object({
144
- classId: z.string(),
145
158
  folderId: z.string(),
146
159
  files: z.array(directFileSchema),
147
160
  })
148
161
  )
149
- .mutation(async ({ ctx, input }) => {
150
- if (!ctx.user) {
151
- throw new Error("You must be logged in to upload files");
152
- }
153
- return getFolderUploadUrls(
154
- ctx.user.id,
155
- input.classId,
156
- input.folderId,
157
- input.files
158
- );
159
- }),
162
+ .mutation(({ ctx, input }) =>
163
+ getFolderUploadUrls(ctx.user!.id, {
164
+ folderId: input.folderId,
165
+ files: input.files,
166
+ })
167
+ ),
160
168
 
161
- confirmFolderUpload: protectedTeacherProcedure
169
+ /** Acknowledge a direct upload completed (or failed). */
170
+ confirmFolderUpload: protectedProcedure
162
171
  .input(
163
172
  z.object({
164
173
  fileId: z.string(),
165
174
  uploadSuccess: z.boolean(),
175
+ errorMessage: z.string().optional(),
166
176
  })
167
177
  )
168
- .mutation(async ({ ctx, input }) => {
169
- if (!ctx.user) {
170
- throw new Error("You must be logged in to confirm uploads");
171
- }
172
- const { confirmDirectUpload } = await import("../lib/fileUpload.js");
173
- await confirmDirectUpload(input.fileId, input.uploadSuccess);
174
- return { success: true };
175
- }),
178
+ .mutation(({ ctx, input }) =>
179
+ confirmFolderUpload(ctx.user!.id, {
180
+ fileId: input.fileId,
181
+ uploadSuccess: input.uploadSuccess,
182
+ errorMessage: input.errorMessage,
183
+ })
184
+ ),
176
185
  });
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Permissions router. Backs the "Share" dialog and the "Shared with me" view.
3
+ *
4
+ * Permission semantics live in `services/permissions.ts`; this router is just
5
+ * a thin Zod-validated tRPC surface over those helpers.
6
+ *
7
+ * v1 grants only on folders. Files inherit from their parent folder. Adding
8
+ * file-level grants later is a non-breaking, additive change.
9
+ */
10
+ import { z } from "zod";
11
+ import { createTRPCRouter, protectedProcedure } from "../trpc.js";
12
+ import {
13
+ grantPermission,
14
+ listPermissions,
15
+ listSharedWithMe,
16
+ revokePermission,
17
+ type PermissionPrincipalInput,
18
+ } from "../services/permissions.js";
19
+
20
+ const principalSchema: z.ZodType<PermissionPrincipalInput> = z.discriminatedUnion(
21
+ "kind",
22
+ [
23
+ z.object({ kind: z.literal("USER"), userId: z.string() }),
24
+ z.object({ kind: z.literal("TAG"), tagId: z.string() }),
25
+ z.object({ kind: z.literal("CLASS"), classId: z.string() }),
26
+ z.object({ kind: z.literal("CLASS_TEACHERS"), classId: z.string() }),
27
+ z.object({ kind: z.literal("CLASS_STUDENTS"), classId: z.string() }),
28
+ z.object({ kind: z.literal("DEPARTMENT"), departmentId: z.string() }),
29
+ z.object({ kind: z.literal("SCHOOL"), schoolId: z.string() }),
30
+ ]
31
+ );
32
+
33
+ const levelSchema = z.enum(["READ", "WRITE"]);
34
+
35
+ export const permissionsRouter = createTRPCRouter({
36
+ /** List all explicit grants on a folder. Caller must have WRITE. */
37
+ list: protectedProcedure
38
+ .input(z.object({ folderId: z.string() }))
39
+ .query(({ ctx, input }) =>
40
+ listPermissions(ctx.user!.id, input.folderId)
41
+ ),
42
+
43
+ /** Create a new explicit grant on a folder. */
44
+ grant: protectedProcedure
45
+ .input(
46
+ z.object({
47
+ folderId: z.string(),
48
+ principal: principalSchema,
49
+ level: levelSchema,
50
+ })
51
+ )
52
+ .mutation(({ ctx, input }) =>
53
+ grantPermission(ctx.user!.id, {
54
+ folderId: input.folderId,
55
+ principal: input.principal,
56
+ level: input.level,
57
+ })
58
+ ),
59
+
60
+ /** Remove an explicit grant. Allowed for the granter or anyone with WRITE on the folder. */
61
+ revoke: protectedProcedure
62
+ .input(z.object({ permissionId: z.string() }))
63
+ .mutation(({ ctx, input }) =>
64
+ revokePermission(ctx.user!.id, input.permissionId)
65
+ ),
66
+
67
+ /** Drive-style "Shared with me" — top-most folders explicitly granted to the caller. */
68
+ listSharedWithMe: protectedProcedure.query(({ ctx }) =>
69
+ listSharedWithMe(ctx.user!.id)
70
+ ),
71
+ });