@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.
- package/dist/middleware/security.d.ts.map +1 -1
- package/dist/middleware/security.js +3 -3
- package/dist/middleware/security.js.map +1 -1
- package/dist/models/agenda.d.ts +2 -2
- package/dist/models/announcement.d.ts +2 -2
- package/dist/models/assignment.d.ts +33 -33
- package/dist/models/class.d.ts +2 -2
- package/dist/models/cloudStorage.d.ts +14 -1
- package/dist/models/cloudStorage.d.ts.map +1 -1
- package/dist/models/cloudStorage.js +123 -3
- package/dist/models/cloudStorage.js.map +1 -1
- package/dist/models/event.d.ts +3 -3
- package/dist/models/file.d.ts +27 -26
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/file.js +4 -39
- package/dist/models/file.js.map +1 -1
- package/dist/models/labChat.d.ts +6 -6
- package/dist/models/section.d.ts +1 -1
- package/dist/routers/_app.d.ts +354 -100
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/agenda.d.ts +2 -2
- package/dist/routers/assignment.d.ts +31 -31
- package/dist/routers/class.d.ts +5 -5
- package/dist/routers/event.d.ts +2 -2
- package/dist/routers/file.d.ts +2 -2
- package/dist/routers/folder.d.ts +24 -0
- package/dist/routers/folder.d.ts.map +1 -1
- package/dist/routers/folder.js +27 -5
- package/dist/routers/folder.js.map +1 -1
- package/dist/routers/labChat.d.ts +2 -2
- package/dist/routers/newtonChat.d.ts +1 -1
- package/dist/routers/permissions.d.ts +120 -0
- package/dist/routers/permissions.d.ts.map +1 -0
- package/dist/routers/permissions.js +50 -0
- package/dist/routers/permissions.js.map +1 -0
- package/dist/routers/tag.d.ts +5 -5
- package/dist/services/agenda.d.ts +2 -2
- package/dist/services/assignment.d.ts +31 -31
- package/dist/services/class.d.ts +5 -5
- package/dist/services/class.js +3 -3
- package/dist/services/class.js.map +1 -1
- package/dist/services/event.d.ts +2 -2
- package/dist/services/file.d.ts +22 -5
- package/dist/services/file.d.ts.map +1 -1
- package/dist/services/file.js +81 -45
- package/dist/services/file.js.map +1 -1
- package/dist/services/folder.d.ts +97 -2
- package/dist/services/folder.d.ts.map +1 -1
- package/dist/services/folder.js +158 -87
- package/dist/services/folder.js.map +1 -1
- package/dist/services/labChat.d.ts +2 -2
- package/dist/services/permissions.d.ts +188 -0
- package/dist/services/permissions.d.ts.map +1 -0
- package/dist/services/permissions.js +661 -0
- package/dist/services/permissions.js.map +1 -0
- package/package.json +1 -1
- package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +93 -0
- package/prisma/schema.prisma +74 -0
- package/src/middleware/security.ts +1 -1
- package/src/models/cloudStorage.ts +134 -1
- package/src/models/file.ts +2 -37
- package/src/routers/_app.ts +2 -0
- package/src/routers/folder.ts +32 -2
- package/src/routers/permissions.ts +71 -0
- package/src/services/class.ts +1 -1
- package/src/services/file.ts +76 -47
- package/src/services/folder.ts +180 -91
- package/src/services/permissions.ts +778 -0
- package/tests/unit/services/permissions.test.ts +463 -0
package/dist/services/folder.js
CHANGED
|
@@ -11,91 +11,84 @@
|
|
|
11
11
|
* endpoints — no duplicate routers.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
!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]="
|
|
14
|
+
!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]="338c145a-d8eb-5931-9a42-2a9c7c9acc94")}catch(e){}}();
|
|
15
15
|
import { TRPCError } from "@trpc/server";
|
|
16
16
|
import { prisma } from "../lib/prisma.js";
|
|
17
17
|
import { createSubFolder, deleteFolderById, findFolderById, findFolderWithContents, findOrCreateScopeRoot, listFolderBreadcrumbs, renameFolder, resolveFolderScope, } from "../models/cloudStorage.js";
|
|
18
18
|
import { createDirectUploadFiles, confirmDirectUpload, } from "../lib/fileUpload.js";
|
|
19
|
+
import { getScopeDefault, listSharedWithMe, requireFolderAccess as requireFolderAccessResolved, } from "./permissions.js";
|
|
20
|
+
/**
|
|
21
|
+
* Scope-default-only check: used when there is no folder id yet (browsing or
|
|
22
|
+
* creating at the scope root). Does *not* consult explicit grants — those only
|
|
23
|
+
* apply to specific folders. Explicit-grant access to sub-folders surfaces via
|
|
24
|
+
* the "Shared with me" virtual scope, not by being a scope member.
|
|
25
|
+
*/
|
|
19
26
|
async function requireScopeAccess(userId, scope, level) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
teachers: { where: { id: userId }, select: { id: true } },
|
|
28
|
-
students: { where: { id: userId }, select: { id: true } },
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
if (!classData) {
|
|
32
|
-
throw new TRPCError({ code: "NOT_FOUND", message: "Class not found" });
|
|
33
|
-
}
|
|
34
|
-
const isTeacher = classData.teachers.length > 0;
|
|
35
|
-
const isStudent = classData.students.length > 0;
|
|
36
|
-
const isSchoolAdmin = classData.schoolId
|
|
37
|
-
? Boolean(await prisma.schoolMembership.findFirst({
|
|
38
|
-
where: {
|
|
39
|
-
schoolId: classData.schoolId,
|
|
40
|
-
userId,
|
|
41
|
-
role: "ADMIN",
|
|
42
|
-
status: "ACTIVE",
|
|
43
|
-
},
|
|
44
|
-
select: { id: true },
|
|
45
|
-
}))
|
|
46
|
-
: false;
|
|
47
|
-
if (!isTeacher && !isStudent && !isSchoolAdmin) {
|
|
48
|
-
throw new TRPCError({
|
|
49
|
-
code: "FORBIDDEN",
|
|
50
|
-
message: "You don't have access to this class",
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
const canWrite = isTeacher || isSchoolAdmin;
|
|
54
|
-
if (level === "WRITE" && !canWrite) {
|
|
55
|
-
throw new TRPCError({
|
|
56
|
-
code: "FORBIDDEN",
|
|
57
|
-
message: "Only class teachers can modify class files",
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
return { scope, canWrite };
|
|
61
|
-
}
|
|
62
|
-
case "SCHOOL_STORAGE": {
|
|
63
|
-
const membership = await prisma.schoolMembership.findFirst({
|
|
64
|
-
where: { schoolId: scope.schoolId, userId, status: "ACTIVE" },
|
|
65
|
-
select: { role: true },
|
|
66
|
-
});
|
|
67
|
-
if (!membership) {
|
|
68
|
-
throw new TRPCError({
|
|
69
|
-
code: "FORBIDDEN",
|
|
70
|
-
message: "You aren't a member of this institution",
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
const canWrite = membership.role === "ADMIN" || membership.role === "TEACHER";
|
|
74
|
-
if (level === "WRITE" && !canWrite) {
|
|
75
|
-
throw new TRPCError({
|
|
76
|
-
code: "FORBIDDEN",
|
|
77
|
-
message: "Only teachers and admins can modify institution storage",
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
return { scope, canWrite };
|
|
81
|
-
}
|
|
82
|
-
case "PERSONAL": {
|
|
83
|
-
if (scope.userId !== userId) {
|
|
84
|
-
throw new TRPCError({
|
|
85
|
-
code: "FORBIDDEN",
|
|
86
|
-
message: "You can only access your own personal storage",
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
return { scope, canWrite: true };
|
|
27
|
+
if (scope.kind === "CLASS") {
|
|
28
|
+
const classExists = await prisma.class.findFirst({
|
|
29
|
+
where: { id: scope.classId },
|
|
30
|
+
select: { id: true },
|
|
31
|
+
});
|
|
32
|
+
if (!classExists) {
|
|
33
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Class not found" });
|
|
90
34
|
}
|
|
91
35
|
}
|
|
36
|
+
const granted = await getScopeDefault(userId, scope);
|
|
37
|
+
if (!granted) {
|
|
38
|
+
throw new TRPCError({
|
|
39
|
+
code: "FORBIDDEN",
|
|
40
|
+
message: scope.kind === "CLASS"
|
|
41
|
+
? "You don't have access to this class"
|
|
42
|
+
: scope.kind === "SCHOOL_STORAGE"
|
|
43
|
+
? "You aren't a member of this institution"
|
|
44
|
+
: "You can only access your own personal storage",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (level === "WRITE" && granted !== "WRITE") {
|
|
48
|
+
throw new TRPCError({
|
|
49
|
+
code: "FORBIDDEN",
|
|
50
|
+
message: scope.kind === "CLASS"
|
|
51
|
+
? "Only class teachers can modify class files"
|
|
52
|
+
: "Only teachers and admins can modify institution storage",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Scope-default-only path; access cannot be via a grant here.
|
|
56
|
+
return { scope, canWrite: granted === "WRITE", viaGrant: false };
|
|
92
57
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Folder-level access check. Delegates to the central resolver which consults
|
|
60
|
+
* explicit grants in addition to the scope default. Returns the scope and a
|
|
61
|
+
* canWrite flag indicating whether the user has WRITE here (used by the UI to
|
|
62
|
+
* gate the "New folder" / "Upload" buttons during a READ-level browse).
|
|
63
|
+
*
|
|
64
|
+
* Pass `mutatesSelf: true` for actions that target the folder itself (rename,
|
|
65
|
+
* delete, move-this-folder). Drive-style: explicit grants don't authorize
|
|
66
|
+
* those — only scope-default WRITE does. Mutations of children should NOT
|
|
67
|
+
* pass this flag.
|
|
68
|
+
*/
|
|
69
|
+
export async function requireFolderAccess(userId, folderId, level, opts = {}) {
|
|
70
|
+
const result = await requireFolderAccessResolved(userId, folderId, level, opts);
|
|
71
|
+
if (!result.scope) {
|
|
72
|
+
// resolveFolderScope returns null only for missing folders; the resolver
|
|
73
|
+
// would have already thrown NOT_FOUND in that case.
|
|
96
74
|
throw new TRPCError({ code: "NOT_FOUND", message: "Folder not found" });
|
|
97
75
|
}
|
|
98
|
-
|
|
76
|
+
let canWrite = result.scopeDefault === "WRITE";
|
|
77
|
+
if (!canWrite && level !== "WRITE") {
|
|
78
|
+
// We came in with READ; check whether the user *also* has WRITE so the UI
|
|
79
|
+
// can show the right affordances. Cheaper to do this only when needed.
|
|
80
|
+
try {
|
|
81
|
+
await requireFolderAccessResolved(userId, folderId, "WRITE");
|
|
82
|
+
canWrite = true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
canWrite = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (level === "WRITE") {
|
|
89
|
+
canWrite = true;
|
|
90
|
+
}
|
|
91
|
+
return { scope: result.scope, canWrite, viaGrant: result.viaGrant };
|
|
99
92
|
}
|
|
100
93
|
function scopesEqual(a, b) {
|
|
101
94
|
if (a.kind !== b.kind)
|
|
@@ -113,22 +106,28 @@ function scopesEqual(a, b) {
|
|
|
113
106
|
export async function browseFolder(userId, input) {
|
|
114
107
|
let scope;
|
|
115
108
|
let folderId;
|
|
109
|
+
let canWrite;
|
|
110
|
+
let viaGrant;
|
|
116
111
|
if ("folderId" in input) {
|
|
117
112
|
folderId = input.folderId;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
scope =
|
|
113
|
+
// `requireFolderAccess` consults explicit grants in addition to the scope
|
|
114
|
+
// default, so users can drill into folders shared with them via "Shared
|
|
115
|
+
// with me" without being scope members.
|
|
116
|
+
const access = await requireFolderAccess(userId, folderId, "READ");
|
|
117
|
+
scope = access.scope;
|
|
118
|
+
canWrite = access.canWrite;
|
|
119
|
+
viaGrant = access.viaGrant;
|
|
123
120
|
}
|
|
124
121
|
else {
|
|
125
122
|
scope = input.scope;
|
|
123
|
+
const access = await requireScopeAccess(userId, scope, "READ");
|
|
124
|
+
canWrite = access.canWrite;
|
|
125
|
+
viaGrant = false;
|
|
126
126
|
}
|
|
127
|
-
const access = await requireScopeAccess(userId, scope, "READ");
|
|
128
127
|
const targetId = folderId ?? (await findOrCreateScopeRoot(scope)).id;
|
|
129
128
|
const [folder, breadcrumbs] = await Promise.all([
|
|
130
129
|
findFolderWithContents(targetId),
|
|
131
|
-
listFolderBreadcrumbs(targetId),
|
|
130
|
+
listFolderBreadcrumbs(targetId, userId),
|
|
132
131
|
]);
|
|
133
132
|
if (!folder) {
|
|
134
133
|
throw new TRPCError({ code: "NOT_FOUND", message: "Folder not found" });
|
|
@@ -136,8 +135,61 @@ export async function browseFolder(userId, input) {
|
|
|
136
135
|
return {
|
|
137
136
|
folder,
|
|
138
137
|
breadcrumbs,
|
|
139
|
-
canWrite
|
|
138
|
+
canWrite,
|
|
140
139
|
scope,
|
|
140
|
+
/**
|
|
141
|
+
* When the viewer's access comes through an explicit grant (not scope
|
|
142
|
+
* membership), the UI should label the root as "Shared with me" rather
|
|
143
|
+
* than the underlying scope's title. The grantee may not even be a
|
|
144
|
+
* member of that scope — showing "My Files" or "Class Files" would be
|
|
145
|
+
* misleading. Null when the viewer is a regular scope member.
|
|
146
|
+
*/
|
|
147
|
+
displayRoot: viaGrant
|
|
148
|
+
? { kind: "SHARED_WITH_ME", name: "Shared with me" }
|
|
149
|
+
: null,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Drive-style "Shared with me" — surfaces every top-most folder/file the user
|
|
154
|
+
* has an explicit grant on (filtering out anything already covered by their
|
|
155
|
+
* scope defaults). Shaped like a folder so the existing folder-browser UI can
|
|
156
|
+
* render it without special cases.
|
|
157
|
+
*
|
|
158
|
+
* The pseudo-folder has a synthetic id (`shared-with-me`) and the foreign-key
|
|
159
|
+
* columns set to null — the UI should never try to mutate it directly. The
|
|
160
|
+
* union shape matches `browseFolder`'s return type so the same client query
|
|
161
|
+
* can render either.
|
|
162
|
+
*/
|
|
163
|
+
export async function browseSharedWithMe(userId) {
|
|
164
|
+
const { folders } = await listSharedWithMe(userId);
|
|
165
|
+
return {
|
|
166
|
+
folder: {
|
|
167
|
+
id: "shared-with-me",
|
|
168
|
+
name: "Shared with me",
|
|
169
|
+
color: null,
|
|
170
|
+
classId: null,
|
|
171
|
+
schoolStorageId: null,
|
|
172
|
+
personalOwnerId: null,
|
|
173
|
+
scope: "GENERIC",
|
|
174
|
+
parentFolderId: null,
|
|
175
|
+
parentFolder: null,
|
|
176
|
+
childFolders: folders.map((f) => ({
|
|
177
|
+
id: f.id,
|
|
178
|
+
name: f.name,
|
|
179
|
+
color: f.color,
|
|
180
|
+
_count: { files: 0, childFolders: 0 },
|
|
181
|
+
})),
|
|
182
|
+
// No file-level grants in v1 — files always live under a granted folder.
|
|
183
|
+
files: [],
|
|
184
|
+
},
|
|
185
|
+
breadcrumbs: [{ id: "shared-with-me", name: "Shared with me" }],
|
|
186
|
+
canWrite: false,
|
|
187
|
+
scope: { kind: "PERSONAL", userId },
|
|
188
|
+
/** Same `displayRoot` shape as `browseFolder` so the client union is uniform. */
|
|
189
|
+
displayRoot: {
|
|
190
|
+
kind: "SHARED_WITH_ME",
|
|
191
|
+
name: "Shared with me",
|
|
192
|
+
},
|
|
141
193
|
};
|
|
142
194
|
}
|
|
143
195
|
export async function getRootFolder(userId, scope) {
|
|
@@ -192,7 +244,11 @@ export async function updateFolderRecord(userId, input) {
|
|
|
192
244
|
message: "Cannot rename a root folder",
|
|
193
245
|
});
|
|
194
246
|
}
|
|
195
|
-
|
|
247
|
+
// Rename targets the folder itself — Drive-style guard reserves this for
|
|
248
|
+
// scope-default WRITE (explicit grants don't authorize renaming the share).
|
|
249
|
+
await requireFolderAccess(userId, input.folderId, "WRITE", {
|
|
250
|
+
mutatesSelf: true,
|
|
251
|
+
});
|
|
196
252
|
return renameFolder(input.folderId, input.name?.trim() ?? folder.name, input.color);
|
|
197
253
|
}
|
|
198
254
|
export async function deleteFolder(userId, folderId) {
|
|
@@ -206,7 +262,8 @@ export async function deleteFolder(userId, folderId) {
|
|
|
206
262
|
message: "Cannot delete a root folder",
|
|
207
263
|
});
|
|
208
264
|
}
|
|
209
|
-
|
|
265
|
+
// Delete targets the folder itself — same Drive-style guard as rename.
|
|
266
|
+
await requireFolderAccess(userId, folderId, "WRITE", { mutatesSelf: true });
|
|
210
267
|
await deleteFolderById(folderId);
|
|
211
268
|
return { success: true };
|
|
212
269
|
}
|
|
@@ -230,7 +287,12 @@ export async function moveFolder(userId, input) {
|
|
|
230
287
|
message: "Cannot move folders between scopes",
|
|
231
288
|
});
|
|
232
289
|
}
|
|
233
|
-
|
|
290
|
+
// Moving the folder targets it (and the new parent's contents). Source-side
|
|
291
|
+
// is mutates-self; target-side is regular WRITE on the new parent.
|
|
292
|
+
await requireFolderAccess(userId, input.folderId, "WRITE", {
|
|
293
|
+
mutatesSelf: true,
|
|
294
|
+
});
|
|
295
|
+
await requireFolderAccess(userId, input.targetParentFolderId, "WRITE");
|
|
234
296
|
// Don't let a folder land under one of its own descendants.
|
|
235
297
|
let cursor = input.targetParentFolderId;
|
|
236
298
|
while (cursor) {
|
|
@@ -292,6 +354,10 @@ export async function listUserScopes(userId) {
|
|
|
292
354
|
},
|
|
293
355
|
orderBy: { name: "asc" },
|
|
294
356
|
});
|
|
357
|
+
// Light counts for the "Shared with me" bucket so the UI can hide the entry
|
|
358
|
+
// when there's nothing to show. Cheap to compute — just counts top-most
|
|
359
|
+
// grants, not folder/file metadata.
|
|
360
|
+
const sharedWithMe = await listSharedWithMe(userId);
|
|
295
361
|
return {
|
|
296
362
|
personal: { kind: "PERSONAL", userId },
|
|
297
363
|
schools: memberships.map((m) => ({
|
|
@@ -301,7 +367,12 @@ export async function listUserScopes(userId) {
|
|
|
301
367
|
role: m.role,
|
|
302
368
|
})),
|
|
303
369
|
classes,
|
|
370
|
+
sharedWithMe: {
|
|
371
|
+
kind: "SHARED_WITH_ME",
|
|
372
|
+
folderCount: sharedWithMe.folders.length,
|
|
373
|
+
fileCount: sharedWithMe.files.length,
|
|
374
|
+
},
|
|
304
375
|
};
|
|
305
376
|
}
|
|
306
377
|
//# sourceMappingURL=folder.js.map
|
|
307
|
-
//# debugId=
|
|
378
|
+
//# debugId=338c145a-d8eb-5931-9a42-2a9c7c9acc94
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"folder.js","sources":["services/folder.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Folder service — single source of truth for folders across every scope.\n *\n * A `Folder` can live inside one of four scopes (see `models/cloudStorage.ts`):\n * class files, school storage, school documents, or a user's personal storage.\n * Permissions and ownership are derived from the scope by walking up the tree\n * to the root, so callers can either pass a `scope` (when there is no folder\n * id yet, e.g. browsing the root) or just a `folderId` (everything else).\n *\n * The class files page and the global cloud-storage view both consume these\n * endpoints — no duplicate routers.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { prisma } from \"../lib/prisma.js\";\nimport {\n CloudScope,\n createSubFolder,\n deleteFolderById,\n findFolderById,\n findFolderWithContents,\n findOrCreateScopeRoot,\n listFolderBreadcrumbs,\n renameFolder,\n resolveFolderScope,\n} from \"../models/cloudStorage.js\";\nimport {\n createDirectUploadFiles,\n type DirectFileData,\n confirmDirectUpload,\n} from \"../lib/fileUpload.js\";\n\ntype AccessLevel = \"READ\" | \"WRITE\";\n\ninterface AccessResult {\n scope: CloudScope;\n canWrite: boolean;\n}\n\nasync function requireScopeAccess(\n userId: string,\n scope: CloudScope,\n level: AccessLevel\n): Promise<AccessResult> {\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) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Class not found\" });\n }\n const isTeacher = classData.teachers.length > 0;\n const isStudent = classData.students.length > 0;\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 (!isTeacher && !isStudent && !isSchoolAdmin) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You don't have access to this class\",\n });\n }\n const canWrite = isTeacher || isSchoolAdmin;\n if (level === \"WRITE\" && !canWrite) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only class teachers can modify class files\",\n });\n }\n return { scope, canWrite };\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) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You aren't a member of this institution\",\n });\n }\n const canWrite =\n membership.role === \"ADMIN\" || membership.role === \"TEACHER\";\n if (level === \"WRITE\" && !canWrite) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only teachers and admins can modify institution storage\",\n });\n }\n return { scope, canWrite };\n }\n case \"PERSONAL\": {\n if (scope.userId !== userId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You can only access your own personal storage\",\n });\n }\n return { scope, canWrite: true };\n }\n }\n}\n\nexport async function requireFolderAccess(\n userId: string,\n folderId: string,\n level: AccessLevel\n) {\n const scope = await resolveFolderScope(folderId);\n if (!scope) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n return requireScopeAccess(userId, scope, level);\n}\n\nfunction scopesEqual(a: CloudScope, b: CloudScope): boolean {\n if (a.kind !== b.kind) return false;\n switch (a.kind) {\n case \"CLASS\":\n return a.classId === (b as typeof a).classId;\n case \"SCHOOL_STORAGE\":\n return a.schoolId === (b as typeof a).schoolId;\n case \"PERSONAL\":\n return a.userId === (b as typeof a).userId;\n }\n}\n\n/** Browse a folder. Pass `folderId` to drill into a specific folder, or a `scope` to land on its root. */\nexport async function browseFolder(\n userId: string,\n input: { folderId: string } | { scope: CloudScope }\n) {\n let scope: CloudScope;\n let folderId: string | undefined;\n\n if (\"folderId\" in input) {\n folderId = input.folderId;\n const resolved = await resolveFolderScope(input.folderId);\n if (!resolved) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n scope = resolved;\n } else {\n scope = input.scope;\n }\n\n const access = await requireScopeAccess(userId, scope, \"READ\");\n const targetId = folderId ?? (await findOrCreateScopeRoot(scope)).id;\n\n const [folder, breadcrumbs] = await Promise.all([\n findFolderWithContents(targetId),\n listFolderBreadcrumbs(targetId),\n ]);\n\n if (!folder) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n\n return {\n folder,\n breadcrumbs,\n canWrite: access.canWrite,\n scope,\n };\n}\n\nexport async function getRootFolder(userId: string, scope: CloudScope) {\n const result = await browseFolder(userId, { scope });\n return result.folder;\n}\n\nexport async function getFolder(userId: string, folderId: string) {\n const result = await browseFolder(userId, { folderId });\n return result.folder;\n}\n\nexport async function getFolderBreadcrumbs(userId: string, folderId: string) {\n await requireFolderAccess(userId, folderId, \"READ\");\n return listFolderBreadcrumbs(folderId);\n}\n\nexport async function getFolderParents(userId: string, folderId: string) {\n // Permission: user can read this folder.\n await requireFolderAccess(userId, folderId, \"READ\");\n const breadcrumbs = await listFolderBreadcrumbs(folderId);\n // Existing API returned full folder records — re-fetch them.\n return Promise.all(\n breadcrumbs.map((b) => findFolderById(b.id))\n );\n}\n\nexport async function createFolderRecord(\n userId: string,\n input: {\n name: string;\n parentFolderId?: string | null;\n color?: string;\n scope?: CloudScope;\n }\n) {\n let parentId: string;\n if (input.parentFolderId) {\n parentId = input.parentFolderId;\n await requireFolderAccess(userId, parentId, \"WRITE\");\n } else {\n if (!input.scope) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"scope is required when parentFolderId is omitted\",\n });\n }\n await requireScopeAccess(userId, input.scope, \"WRITE\");\n parentId = (await findOrCreateScopeRoot(input.scope)).id;\n }\n return createSubFolder({\n name: input.name.trim(),\n parentFolderId: parentId,\n color: input.color,\n });\n}\n\nexport async function updateFolderRecord(\n userId: string,\n input: { folderId: string; name?: string; color?: string }\n) {\n const folder = await findFolderById(input.folderId);\n if (!folder) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n if (!folder.parentFolderId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot rename a root folder\",\n });\n }\n await requireFolderAccess(userId, input.folderId, \"WRITE\");\n return renameFolder(\n input.folderId,\n input.name?.trim() ?? folder.name,\n input.color\n );\n}\n\nexport async function deleteFolder(userId: string, folderId: string) {\n const folder = await findFolderById(folderId);\n if (!folder) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n if (!folder.parentFolderId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot delete a root folder\",\n });\n }\n await requireFolderAccess(userId, folderId, \"WRITE\");\n await deleteFolderById(folderId);\n return { success: true };\n}\n\nexport async function moveFolder(\n userId: string,\n input: { folderId: string; targetParentFolderId: string }\n) {\n if (input.folderId === input.targetParentFolderId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot move a folder into itself\",\n });\n }\n const [sourceScope, targetScope] = await Promise.all([\n resolveFolderScope(input.folderId),\n resolveFolderScope(input.targetParentFolderId),\n ]);\n if (!sourceScope || !targetScope) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n if (!scopesEqual(sourceScope, targetScope)) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot move folders between scopes\",\n });\n }\n await requireScopeAccess(userId, sourceScope, \"WRITE\");\n\n // Don't let a folder land under one of its own descendants.\n let cursor: string | null = input.targetParentFolderId;\n while (cursor) {\n if (cursor === input.folderId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot move a folder into its own descendant\",\n });\n }\n const node = await prisma.folder.findUnique({\n where: { id: cursor },\n select: { parentFolderId: true },\n });\n cursor = node?.parentFolderId ?? null;\n }\n\n await prisma.folder.update({\n where: { id: input.folderId },\n data: { parentFolderId: input.targetParentFolderId },\n });\n return { success: true };\n}\n\nexport async function getFolderUploadUrls(\n userId: string,\n input: { folderId: string; files: DirectFileData[] }\n) {\n await requireFolderAccess(userId, input.folderId, \"WRITE\");\n return createDirectUploadFiles(input.files, userId, input.folderId);\n}\n\nexport async function confirmFolderUpload(\n _userId: string,\n input: { fileId: string; uploadSuccess: boolean; errorMessage?: string }\n) {\n // Permission was already checked at upload-url issuance — the file record\n // exists with PENDING status, and the user holds the session id needed to\n // PUT bytes. Confirming flips status, so we just trust the call.\n await confirmDirectUpload(\n input.fileId,\n input.uploadSuccess,\n input.errorMessage\n );\n return { success: true };\n}\n\n/** What scopes can this user pick from in cloud-storage UIs? */\nexport async function listUserScopes(userId: string) {\n const memberships = await prisma.schoolMembership.findMany({\n where: { userId, status: \"ACTIVE\" },\n select: {\n role: true,\n school: {\n select: { id: true, name: true, logoId: true },\n },\n },\n orderBy: { createdAt: \"asc\" },\n });\n\n const classes = await prisma.class.findMany({\n where: {\n OR: [\n { teachers: { some: { id: userId } } },\n { students: { some: { id: userId } } },\n ],\n },\n select: {\n id: true,\n name: true,\n subject: true,\n section: true,\n color: true,\n schoolId: true,\n },\n orderBy: { name: \"asc\" },\n });\n\n return {\n personal: { kind: \"PERSONAL\" as const, userId },\n schools: memberships.map((m) => ({\n id: m.school.id,\n name: m.school.name,\n logoId: m.school.logoId,\n role: m.role,\n })),\n classes,\n };\n}\n"],"names":[],"mappings":"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","debug_id":"1e1a55f0-d115-5d93-9636-a89ae577bf45"}
|
|
1
|
+
{"version":3,"file":"folder.js","sources":["services/folder.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Folder service — single source of truth for folders across every scope.\n *\n * A `Folder` can live inside one of four scopes (see `models/cloudStorage.ts`):\n * class files, school storage, school documents, or a user's personal storage.\n * Permissions and ownership are derived from the scope by walking up the tree\n * to the root, so callers can either pass a `scope` (when there is no folder\n * id yet, e.g. browsing the root) or just a `folderId` (everything else).\n *\n * The class files page and the global cloud-storage view both consume these\n * endpoints — no duplicate routers.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { prisma } from \"../lib/prisma.js\";\nimport {\n CloudScope,\n createSubFolder,\n deleteFolderById,\n findFolderById,\n findFolderWithContents,\n findOrCreateScopeRoot,\n listFolderBreadcrumbs,\n renameFolder,\n resolveFolderScope,\n} from \"../models/cloudStorage.js\";\nimport {\n createDirectUploadFiles,\n type DirectFileData,\n confirmDirectUpload,\n} from \"../lib/fileUpload.js\";\nimport {\n type AccessLevel,\n getScopeDefault,\n listSharedWithMe,\n requireFolderAccess as requireFolderAccessResolved,\n} from \"./permissions.js\";\n\ninterface AccessResult {\n scope: CloudScope;\n canWrite: boolean;\n /**\n * True when the viewer's access comes through an explicit grant rather than\n * scope membership. Used by `browseFolder` to surface a \"Shared with me\"\n * heading instead of the underlying scope's title — the grantee shouldn't\n * see the folder labelled with the owner's scope.\n */\n viaGrant: boolean;\n}\n\n/**\n * Scope-default-only check: used when there is no folder id yet (browsing or\n * creating at the scope root). Does *not* consult explicit grants — those only\n * apply to specific folders. Explicit-grant access to sub-folders surfaces via\n * the \"Shared with me\" virtual scope, not by being a scope member.\n */\nasync function requireScopeAccess(\n userId: string,\n scope: CloudScope,\n level: AccessLevel\n): Promise<AccessResult> {\n if (scope.kind === \"CLASS\") {\n const classExists = await prisma.class.findFirst({\n where: { id: scope.classId },\n select: { id: true },\n });\n if (!classExists) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Class not found\" });\n }\n }\n const granted = await getScopeDefault(userId, scope);\n if (!granted) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message:\n scope.kind === \"CLASS\"\n ? \"You don't have access to this class\"\n : scope.kind === \"SCHOOL_STORAGE\"\n ? \"You aren't a member of this institution\"\n : \"You can only access your own personal storage\",\n });\n }\n if (level === \"WRITE\" && granted !== \"WRITE\") {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message:\n scope.kind === \"CLASS\"\n ? \"Only class teachers can modify class files\"\n : \"Only teachers and admins can modify institution storage\",\n });\n }\n // Scope-default-only path; access cannot be via a grant here.\n return { scope, canWrite: granted === \"WRITE\", viaGrant: false };\n}\n\n/**\n * Folder-level access check. Delegates to the central resolver which consults\n * explicit grants in addition to the scope default. Returns the scope and a\n * canWrite flag indicating whether the user has WRITE here (used by the UI to\n * gate the \"New folder\" / \"Upload\" buttons during a READ-level browse).\n *\n * Pass `mutatesSelf: true` for actions that target the folder itself (rename,\n * delete, move-this-folder). Drive-style: explicit grants don't authorize\n * those — only scope-default WRITE does. Mutations of children should NOT\n * pass this flag.\n */\nexport async function requireFolderAccess(\n userId: string,\n folderId: string,\n level: AccessLevel,\n opts: { mutatesSelf?: boolean } = {}\n): Promise<AccessResult> {\n const result = await requireFolderAccessResolved(userId, folderId, level, opts);\n if (!result.scope) {\n // resolveFolderScope returns null only for missing folders; the resolver\n // would have already thrown NOT_FOUND in that case.\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n let canWrite = result.scopeDefault === \"WRITE\";\n if (!canWrite && level !== \"WRITE\") {\n // We came in with READ; check whether the user *also* has WRITE so the UI\n // can show the right affordances. Cheaper to do this only when needed.\n try {\n await requireFolderAccessResolved(userId, folderId, \"WRITE\");\n canWrite = true;\n } catch {\n canWrite = false;\n }\n } else if (level === \"WRITE\") {\n canWrite = true;\n }\n return { scope: result.scope, canWrite, viaGrant: result.viaGrant };\n}\n\nfunction scopesEqual(a: CloudScope, b: CloudScope): boolean {\n if (a.kind !== b.kind) return false;\n switch (a.kind) {\n case \"CLASS\":\n return a.classId === (b as typeof a).classId;\n case \"SCHOOL_STORAGE\":\n return a.schoolId === (b as typeof a).schoolId;\n case \"PERSONAL\":\n return a.userId === (b as typeof a).userId;\n }\n}\n\n/** Browse a folder. Pass `folderId` to drill into a specific folder, or a `scope` to land on its root. */\nexport async function browseFolder(\n userId: string,\n input: { folderId: string } | { scope: CloudScope }\n) {\n let scope: CloudScope;\n let folderId: string | undefined;\n let canWrite: boolean;\n let viaGrant: boolean;\n\n if (\"folderId\" in input) {\n folderId = input.folderId;\n // `requireFolderAccess` consults explicit grants in addition to the scope\n // default, so users can drill into folders shared with them via \"Shared\n // with me\" without being scope members.\n const access = await requireFolderAccess(userId, folderId, \"READ\");\n scope = access.scope;\n canWrite = access.canWrite;\n viaGrant = access.viaGrant;\n } else {\n scope = input.scope;\n const access = await requireScopeAccess(userId, scope, \"READ\");\n canWrite = access.canWrite;\n viaGrant = false;\n }\n\n const targetId = folderId ?? (await findOrCreateScopeRoot(scope)).id;\n\n const [folder, breadcrumbs] = await Promise.all([\n findFolderWithContents(targetId),\n listFolderBreadcrumbs(targetId, userId),\n ]);\n\n if (!folder) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n\n return {\n folder,\n breadcrumbs,\n canWrite,\n scope,\n /**\n * When the viewer's access comes through an explicit grant (not scope\n * membership), the UI should label the root as \"Shared with me\" rather\n * than the underlying scope's title. The grantee may not even be a\n * member of that scope — showing \"My Files\" or \"Class Files\" would be\n * misleading. Null when the viewer is a regular scope member.\n */\n displayRoot: viaGrant\n ? { kind: \"SHARED_WITH_ME\" as const, name: \"Shared with me\" }\n : null,\n };\n}\n\n/**\n * Drive-style \"Shared with me\" — surfaces every top-most folder/file the user\n * has an explicit grant on (filtering out anything already covered by their\n * scope defaults). Shaped like a folder so the existing folder-browser UI can\n * render it without special cases.\n *\n * The pseudo-folder has a synthetic id (`shared-with-me`) and the foreign-key\n * columns set to null — the UI should never try to mutate it directly. The\n * union shape matches `browseFolder`'s return type so the same client query\n * can render either.\n */\nexport async function browseSharedWithMe(userId: string) {\n const { folders } = await listSharedWithMe(userId);\n return {\n folder: {\n id: \"shared-with-me\",\n name: \"Shared with me\",\n color: null,\n classId: null,\n schoolStorageId: null,\n personalOwnerId: null,\n scope: \"GENERIC\" as const,\n parentFolderId: null,\n parentFolder: null,\n childFolders: folders.map((f) => ({\n id: f.id,\n name: f.name,\n color: f.color,\n _count: { files: 0, childFolders: 0 },\n })),\n // No file-level grants in v1 — files always live under a granted folder.\n files: [] as {\n id: string;\n name: string;\n type: string;\n size: number | null;\n path: string;\n uploadedAt: Date | null;\n user: { id: string; username: string } | null;\n }[],\n },\n breadcrumbs: [{ id: \"shared-with-me\", name: \"Shared with me\" }],\n canWrite: false,\n scope: { kind: \"PERSONAL\" as const, userId } satisfies CloudScope,\n /** Same `displayRoot` shape as `browseFolder` so the client union is uniform. */\n displayRoot: {\n kind: \"SHARED_WITH_ME\" as const,\n name: \"Shared with me\",\n },\n };\n}\n\nexport async function getRootFolder(userId: string, scope: CloudScope) {\n const result = await browseFolder(userId, { scope });\n return result.folder;\n}\n\nexport async function getFolder(userId: string, folderId: string) {\n const result = await browseFolder(userId, { folderId });\n return result.folder;\n}\n\nexport async function getFolderBreadcrumbs(userId: string, folderId: string) {\n await requireFolderAccess(userId, folderId, \"READ\");\n return listFolderBreadcrumbs(folderId);\n}\n\nexport async function getFolderParents(userId: string, folderId: string) {\n // Permission: user can read this folder.\n await requireFolderAccess(userId, folderId, \"READ\");\n const breadcrumbs = await listFolderBreadcrumbs(folderId);\n // Existing API returned full folder records — re-fetch them.\n return Promise.all(\n breadcrumbs.map((b) => findFolderById(b.id))\n );\n}\n\nexport async function createFolderRecord(\n userId: string,\n input: {\n name: string;\n parentFolderId?: string | null;\n color?: string;\n scope?: CloudScope;\n }\n) {\n let parentId: string;\n if (input.parentFolderId) {\n parentId = input.parentFolderId;\n await requireFolderAccess(userId, parentId, \"WRITE\");\n } else {\n if (!input.scope) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"scope is required when parentFolderId is omitted\",\n });\n }\n await requireScopeAccess(userId, input.scope, \"WRITE\");\n parentId = (await findOrCreateScopeRoot(input.scope)).id;\n }\n return createSubFolder({\n name: input.name.trim(),\n parentFolderId: parentId,\n color: input.color,\n });\n}\n\nexport async function updateFolderRecord(\n userId: string,\n input: { folderId: string; name?: string; color?: string }\n) {\n const folder = await findFolderById(input.folderId);\n if (!folder) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n if (!folder.parentFolderId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot rename a root folder\",\n });\n }\n // Rename targets the folder itself — Drive-style guard reserves this for\n // scope-default WRITE (explicit grants don't authorize renaming the share).\n await requireFolderAccess(userId, input.folderId, \"WRITE\", {\n mutatesSelf: true,\n });\n return renameFolder(\n input.folderId,\n input.name?.trim() ?? folder.name,\n input.color\n );\n}\n\nexport async function deleteFolder(userId: string, folderId: string) {\n const folder = await findFolderById(folderId);\n if (!folder) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n if (!folder.parentFolderId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot delete a root folder\",\n });\n }\n // Delete targets the folder itself — same Drive-style guard as rename.\n await requireFolderAccess(userId, folderId, \"WRITE\", { mutatesSelf: true });\n await deleteFolderById(folderId);\n return { success: true };\n}\n\nexport async function moveFolder(\n userId: string,\n input: { folderId: string; targetParentFolderId: string }\n) {\n if (input.folderId === input.targetParentFolderId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot move a folder into itself\",\n });\n }\n const [sourceScope, targetScope] = await Promise.all([\n resolveFolderScope(input.folderId),\n resolveFolderScope(input.targetParentFolderId),\n ]);\n if (!sourceScope || !targetScope) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n if (!scopesEqual(sourceScope, targetScope)) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot move folders between scopes\",\n });\n }\n // Moving the folder targets it (and the new parent's contents). Source-side\n // is mutates-self; target-side is regular WRITE on the new parent.\n await requireFolderAccess(userId, input.folderId, \"WRITE\", {\n mutatesSelf: true,\n });\n await requireFolderAccess(userId, input.targetParentFolderId, \"WRITE\");\n\n // Don't let a folder land under one of its own descendants.\n let cursor: string | null = input.targetParentFolderId;\n while (cursor) {\n if (cursor === input.folderId) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot move a folder into its own descendant\",\n });\n }\n const node = await prisma.folder.findUnique({\n where: { id: cursor },\n select: { parentFolderId: true },\n });\n cursor = node?.parentFolderId ?? null;\n }\n\n await prisma.folder.update({\n where: { id: input.folderId },\n data: { parentFolderId: input.targetParentFolderId },\n });\n return { success: true };\n}\n\nexport async function getFolderUploadUrls(\n userId: string,\n input: { folderId: string; files: DirectFileData[] }\n) {\n await requireFolderAccess(userId, input.folderId, \"WRITE\");\n return createDirectUploadFiles(input.files, userId, input.folderId);\n}\n\nexport async function confirmFolderUpload(\n _userId: string,\n input: { fileId: string; uploadSuccess: boolean; errorMessage?: string }\n) {\n // Permission was already checked at upload-url issuance — the file record\n // exists with PENDING status, and the user holds the session id needed to\n // PUT bytes. Confirming flips status, so we just trust the call.\n await confirmDirectUpload(\n input.fileId,\n input.uploadSuccess,\n input.errorMessage\n );\n return { success: true };\n}\n\n/** What scopes can this user pick from in cloud-storage UIs? */\nexport async function listUserScopes(userId: string) {\n const memberships = await prisma.schoolMembership.findMany({\n where: { userId, status: \"ACTIVE\" },\n select: {\n role: true,\n school: {\n select: { id: true, name: true, logoId: true },\n },\n },\n orderBy: { createdAt: \"asc\" },\n });\n\n const classes = await prisma.class.findMany({\n where: {\n OR: [\n { teachers: { some: { id: userId } } },\n { students: { some: { id: userId } } },\n ],\n },\n select: {\n id: true,\n name: true,\n subject: true,\n section: true,\n color: true,\n schoolId: true,\n },\n orderBy: { name: \"asc\" },\n });\n\n // Light counts for the \"Shared with me\" bucket so the UI can hide the entry\n // when there's nothing to show. Cheap to compute — just counts top-most\n // grants, not folder/file metadata.\n const sharedWithMe = await listSharedWithMe(userId);\n\n return {\n personal: { kind: \"PERSONAL\" as const, userId },\n schools: memberships.map((m) => ({\n id: m.school.id,\n name: m.school.name,\n logoId: m.school.logoId,\n role: m.role,\n })),\n classes,\n sharedWithMe: {\n kind: \"SHARED_WITH_ME\" as const,\n folderCount: sharedWithMe.folders.length,\n fileCount: sharedWithMe.files.length,\n },\n };\n}\n"],"names":[],"mappings":"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","debug_id":"338c145a-d8eb-5931-9a42-2a9c7c9acc94"}
|
|
@@ -50,8 +50,8 @@ export declare function createLabChat(userId: string, input: {
|
|
|
50
50
|
id: string;
|
|
51
51
|
createdAt: Date;
|
|
52
52
|
updatedAt: Date;
|
|
53
|
-
conversationId: string;
|
|
54
53
|
classId: string;
|
|
54
|
+
conversationId: string;
|
|
55
55
|
title: string;
|
|
56
56
|
createdById: string;
|
|
57
57
|
}>;
|
|
@@ -103,8 +103,8 @@ export declare function getLabChat(userId: string, labChatId: string): Promise<{
|
|
|
103
103
|
id: string;
|
|
104
104
|
createdAt: Date;
|
|
105
105
|
updatedAt: Date;
|
|
106
|
-
conversationId: string;
|
|
107
106
|
classId: string;
|
|
107
|
+
conversationId: string;
|
|
108
108
|
title: string;
|
|
109
109
|
createdById: string;
|
|
110
110
|
}>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { type CloudScope } from "../models/cloudStorage.js";
|
|
2
|
+
export type AccessLevel = "READ" | "WRITE";
|
|
3
|
+
/**
|
|
4
|
+
* The set of "audiences" a user belongs to that an explicit grant can match.
|
|
5
|
+
* Generated once per request via `expandUserPrincipals` and threaded through
|
|
6
|
+
* the resolver.
|
|
7
|
+
*/
|
|
8
|
+
export interface UserPrincipals {
|
|
9
|
+
userId: string;
|
|
10
|
+
tagIds: string[];
|
|
11
|
+
classTeacherIds: string[];
|
|
12
|
+
classStudentIds: string[];
|
|
13
|
+
/** Union of teacher + student class memberships (i.e. all CLASS principals). */
|
|
14
|
+
classIds: string[];
|
|
15
|
+
departmentIds: string[];
|
|
16
|
+
/** Active school memberships only. */
|
|
17
|
+
schoolIds: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build the audience set for a user. Cheap (5 queries, all id-only) but worth
|
|
21
|
+
* caching per request — most permission checks in a single tRPC call hit this.
|
|
22
|
+
*/
|
|
23
|
+
export declare function expandUserPrincipals(userId: string): Promise<UserPrincipals>;
|
|
24
|
+
/** Walk up to the scope root, collecting folder ids visited along the way (inclusive). */
|
|
25
|
+
declare function findAncestorIds(folderId: string): Promise<string[]>;
|
|
26
|
+
export { findAncestorIds };
|
|
27
|
+
/**
|
|
28
|
+
* Compute the scope default for a user — i.e. the access level the existing
|
|
29
|
+
* scope-derived rules grant, *before* any explicit ACL is consulted.
|
|
30
|
+
*
|
|
31
|
+
* Mirrors the logic that used to live in `requireScopeAccess` in folder.ts.
|
|
32
|
+
* Returns "WRITE" / "READ" / null.
|
|
33
|
+
*/
|
|
34
|
+
export declare function getScopeDefault(userId: string, scope: CloudScope): Promise<AccessLevel | null>;
|
|
35
|
+
export interface AccessResult {
|
|
36
|
+
scope: CloudScope | null;
|
|
37
|
+
scopeDefault: AccessLevel | null;
|
|
38
|
+
/** Truthy if the resolver returned because of an explicit grant (folder or file). */
|
|
39
|
+
viaGrant: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Permission gate for folder-scoped operations. Throws TRPCError on denial,
|
|
43
|
+
* returns the scope (and which path granted access) on success.
|
|
44
|
+
*
|
|
45
|
+
* `opts.mutatesSelf` (Drive-style guard): when true, the action targets the
|
|
46
|
+
* folder *itself* — rename, delete, move-this-folder. Explicit grants do NOT
|
|
47
|
+
* authorize these; only scope-default WRITE does. This means a teacher sharing
|
|
48
|
+
* "Math Resources" with the parents tag can let parents read/edit children of
|
|
49
|
+
* the folder, but parents cannot rename or delete the share root. Mutations
|
|
50
|
+
* targeting *children* of the folder (uploads, sub-folders, child renames)
|
|
51
|
+
* pass `mutatesSelf: false` and flow through the normal grant + ancestor +
|
|
52
|
+
* scope-default path.
|
|
53
|
+
*/
|
|
54
|
+
export declare function requireFolderAccess(userId: string, folderId: string, level: AccessLevel, opts?: {
|
|
55
|
+
mutatesSelf?: boolean;
|
|
56
|
+
}): Promise<AccessResult>;
|
|
57
|
+
/**
|
|
58
|
+
* Permission gate for file-scoped operations. Dispatches on whichever context
|
|
59
|
+
* relation the file carries, so every kind of file (cloud-storage, chat
|
|
60
|
+
* attachment, assignment / submission / annotation / announcement / class-draft
|
|
61
|
+
* attachment, school logo) gets authorized through one entry point.
|
|
62
|
+
*
|
|
63
|
+
* Resolution order — first match wins:
|
|
64
|
+
* 1. **Owner** — `file.userId === user`. Always WRITE.
|
|
65
|
+
* 2. **Folder-bound** — delegate to `requireFolderAccess` (handles scope
|
|
66
|
+
* defaults + explicit grants + ancestor inheritance).
|
|
67
|
+
* 3. **Conversation/message attachment** — any conversation member READs;
|
|
68
|
+
* only the sender (= owner, handled in step 1) WRITEs.
|
|
69
|
+
* 4. **Assignment attachment** — class teacher/admin RW, class student R.
|
|
70
|
+
* 5. **Submission file** — submission's student RW (until grading workflow
|
|
71
|
+
* locks it down), class teacher/admin R.
|
|
72
|
+
* 6. **Annotation file** — class teacher/admin RW, submission's student R.
|
|
73
|
+
* 7. **Announcement attachment** — class teacher/admin RW, class student R.
|
|
74
|
+
* 8. **Class-draft attachment** — class teacher/admin RW (drafts have no
|
|
75
|
+
* students yet in practice).
|
|
76
|
+
* 9. **School logo** — any active school member R, school admin W.
|
|
77
|
+
* 10. **Otherwise** (incl. SDP supporting docs) — owner-only fallback already
|
|
78
|
+
* covered by step 1; deny.
|
|
79
|
+
*/
|
|
80
|
+
export declare function requireFileAccess(userId: string, fileId: string, level: AccessLevel): Promise<AccessResult>;
|
|
81
|
+
/**
|
|
82
|
+
* Top-most-grant computation: surface every folder the user has an explicit
|
|
83
|
+
* grant on whose nearest permissioned ancestor the user does *not* also hold a
|
|
84
|
+
* grant on, AND whose scope default does not already grant access. That second
|
|
85
|
+
* filter avoids "Shared with me" duplicating things you already see in your
|
|
86
|
+
* normal class/school/personal scopes.
|
|
87
|
+
*
|
|
88
|
+
* Files are not surfaced here — file-level grants are deferred to a future
|
|
89
|
+
* feature; in v1 files inherit from their folder.
|
|
90
|
+
*/
|
|
91
|
+
export declare function listSharedWithMe(userId: string): Promise<{
|
|
92
|
+
folders: {
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
color: string | null;
|
|
96
|
+
}[];
|
|
97
|
+
files: never[];
|
|
98
|
+
}>;
|
|
99
|
+
export type PermissionPrincipalInput = {
|
|
100
|
+
kind: "USER";
|
|
101
|
+
userId: string;
|
|
102
|
+
} | {
|
|
103
|
+
kind: "TAG";
|
|
104
|
+
tagId: string;
|
|
105
|
+
} | {
|
|
106
|
+
kind: "CLASS";
|
|
107
|
+
classId: string;
|
|
108
|
+
} | {
|
|
109
|
+
kind: "CLASS_TEACHERS";
|
|
110
|
+
classId: string;
|
|
111
|
+
} | {
|
|
112
|
+
kind: "CLASS_STUDENTS";
|
|
113
|
+
classId: string;
|
|
114
|
+
} | {
|
|
115
|
+
kind: "DEPARTMENT";
|
|
116
|
+
departmentId: string;
|
|
117
|
+
} | {
|
|
118
|
+
kind: "SCHOOL";
|
|
119
|
+
schoolId: string;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Create a new explicit grant on a folder. Validates:
|
|
123
|
+
* - Granter has WRITE on the folder.
|
|
124
|
+
* - The principal is in the same school as the folder's scope (no
|
|
125
|
+
* cross-school leaks). Personal-scope folders cannot be granted to
|
|
126
|
+
* non-USER principals — there's no "school" to check membership against.
|
|
127
|
+
*/
|
|
128
|
+
export declare function grantPermission(grantedById: string, input: {
|
|
129
|
+
folderId: string;
|
|
130
|
+
principal: PermissionPrincipalInput;
|
|
131
|
+
level: AccessLevel;
|
|
132
|
+
}): Promise<{
|
|
133
|
+
level: import(".prisma/client").$Enums.PermissionLevel;
|
|
134
|
+
id: string;
|
|
135
|
+
createdAt: Date;
|
|
136
|
+
schoolId: string | null;
|
|
137
|
+
userId: string | null;
|
|
138
|
+
departmentId: string | null;
|
|
139
|
+
folderId: string;
|
|
140
|
+
principalType: import(".prisma/client").$Enums.PermissionPrincipalType;
|
|
141
|
+
tagId: string | null;
|
|
142
|
+
classId: string | null;
|
|
143
|
+
grantedById: string;
|
|
144
|
+
}>;
|
|
145
|
+
/** Revoke a grant. Allowed for the granter or anyone with WRITE on the folder. */
|
|
146
|
+
export declare function revokePermission(userId: string, permissionId: string): Promise<{
|
|
147
|
+
success: boolean;
|
|
148
|
+
}>;
|
|
149
|
+
/** List all explicit grants on a folder. Caller must have WRITE on it. */
|
|
150
|
+
export declare function listPermissions(userId: string, folderId: string): Promise<({
|
|
151
|
+
school: {
|
|
152
|
+
name: string;
|
|
153
|
+
id: string;
|
|
154
|
+
} | null;
|
|
155
|
+
user: {
|
|
156
|
+
id: string;
|
|
157
|
+
username: string;
|
|
158
|
+
email: string;
|
|
159
|
+
} | null;
|
|
160
|
+
class: {
|
|
161
|
+
name: string;
|
|
162
|
+
id: string;
|
|
163
|
+
section: string;
|
|
164
|
+
} | null;
|
|
165
|
+
department: {
|
|
166
|
+
name: string;
|
|
167
|
+
id: string;
|
|
168
|
+
color: string | null;
|
|
169
|
+
} | null;
|
|
170
|
+
tag: {
|
|
171
|
+
name: string;
|
|
172
|
+
id: string;
|
|
173
|
+
color: string;
|
|
174
|
+
} | null;
|
|
175
|
+
} & {
|
|
176
|
+
level: import(".prisma/client").$Enums.PermissionLevel;
|
|
177
|
+
id: string;
|
|
178
|
+
createdAt: Date;
|
|
179
|
+
schoolId: string | null;
|
|
180
|
+
userId: string | null;
|
|
181
|
+
departmentId: string | null;
|
|
182
|
+
folderId: string;
|
|
183
|
+
principalType: import(".prisma/client").$Enums.PermissionPrincipalType;
|
|
184
|
+
tagId: string | null;
|
|
185
|
+
classId: string | null;
|
|
186
|
+
grantedById: string;
|
|
187
|
+
})[]>;
|
|
188
|
+
//# sourceMappingURL=permissions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.d.ts","sourceRoot":"/","sources":["services/permissions.ts"],"names":[],"mappings":"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"}
|