@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.
- 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/access.d.ts +21 -66
- package/dist/models/access.d.ts.map +1 -1
- package/dist/models/access.js +10 -26
- package/dist/models/access.js.map +1 -1
- package/dist/models/agenda.d.ts +4 -2
- package/dist/models/agenda.d.ts.map +1 -1
- package/dist/models/announcement.d.ts +4 -3
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/announcement.js +2 -3
- package/dist/models/announcement.js.map +1 -1
- package/dist/models/assignment.d.ts +40 -33
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/auth.d.ts +2 -4
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +2 -3
- package/dist/models/auth.js.map +1 -1
- package/dist/models/class.d.ts +34 -6
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +15 -2
- package/dist/models/class.js.map +1 -1
- package/dist/models/cloudStorage.d.ts +153 -0
- package/dist/models/cloudStorage.d.ts.map +1 -0
- package/dist/models/cloudStorage.js +295 -0
- package/dist/models/cloudStorage.js.map +1 -0
- package/dist/models/comment.d.ts +0 -24
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/comment.js +2 -6
- package/dist/models/comment.js.map +1 -1
- package/dist/models/conversation.d.ts +22 -22
- package/dist/models/department.d.ts +154 -0
- package/dist/models/department.d.ts.map +1 -0
- package/dist/models/department.js +135 -0
- package/dist/models/department.js.map +1 -0
- package/dist/models/event.d.ts +10 -4
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/event.js +2 -5
- package/dist/models/event.js.map +1 -1
- package/dist/models/file.d.ts +52 -36
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/file.js +4 -42
- package/dist/models/file.js.map +1 -1
- package/dist/models/folder.d.ts +47 -0
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +11 -9
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/message.d.ts +46 -12
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/message.js +19 -10
- package/dist/models/message.js.map +1 -1
- package/dist/models/newtonChat.d.ts +2 -1
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/school.d.ts +45 -12
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/school.js +19 -2
- package/dist/models/school.js.map +1 -1
- package/dist/models/section.d.ts +2 -1
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/tag.d.ts +117 -0
- package/dist/models/tag.d.ts.map +1 -0
- package/dist/models/tag.js +113 -0
- package/dist/models/tag.js.map +1 -0
- package/dist/models/worksheet.d.ts +3 -0
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +8 -8
- package/dist/routers/_app.d.ts +6704 -5222
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +8 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/agenda.d.ts +4 -2
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +3 -5
- package/dist/routers/announcement.js.map +1 -1
- package/dist/routers/assignment.d.ts +49 -40
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +2 -3
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/class.d.ts +92 -19
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +5 -2
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/conversation.d.ts +5 -5
- package/dist/routers/department.d.ts +293 -0
- package/dist/routers/department.d.ts.map +1 -0
- package/dist/routers/department.js +93 -0
- package/dist/routers/department.js.map +1 -0
- package/dist/routers/event.d.ts +4 -3
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/file.d.ts +10 -8
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +26 -17
- package/dist/routers/file.js.map +1 -1
- package/dist/routers/folder.d.ts +163 -186
- package/dist/routers/folder.d.ts.map +1 -1
- package/dist/routers/folder.js +102 -88
- package/dist/routers/folder.js.map +1 -1
- package/dist/routers/labChat.d.ts +13 -4
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/message.d.ts +20 -2
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/newtonChat.d.ts +10 -1
- package/dist/routers/newtonChat.d.ts.map +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/school.d.ts +160 -17
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +46 -3
- package/dist/routers/school.js.map +1 -1
- package/dist/routers/tag.d.ts +119 -0
- package/dist/routers/tag.d.ts.map +1 -0
- package/dist/routers/tag.js +52 -0
- package/dist/routers/tag.js.map +1 -0
- package/dist/routers/worksheet.d.ts +1 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/seedDatabase.d.ts +0 -1
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/services/agenda.d.ts +4 -2
- package/dist/services/agenda.d.ts.map +1 -1
- package/dist/services/assignment.d.ts +49 -40
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/class.d.ts +92 -19
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +92 -5
- package/dist/services/class.js.map +1 -1
- package/dist/services/conversation.d.ts +5 -5
- package/dist/services/department.d.ts +200 -0
- package/dist/services/department.d.ts.map +1 -0
- package/dist/services/department.js +368 -0
- package/dist/services/department.js.map +1 -0
- package/dist/services/event.d.ts +4 -3
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/file.d.ts +105 -3
- package/dist/services/file.d.ts.map +1 -1
- package/dist/services/file.js +167 -41
- package/dist/services/file.js.map +1 -1
- package/dist/services/folder.d.ts +220 -149
- package/dist/services/folder.d.ts.map +1 -1
- package/dist/services/folder.js +326 -196
- package/dist/services/folder.js.map +1 -1
- package/dist/services/labChat.d.ts +13 -4
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/labChat.js +4 -3
- package/dist/services/labChat.js.map +1 -1
- package/dist/services/message.d.ts +20 -2
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +7 -23
- package/dist/services/message.js.map +1 -1
- package/dist/services/newtonChat.d.ts +9 -0
- package/dist/services/newtonChat.d.ts.map +1 -1
- package/dist/services/newtonChat.js +4 -3
- package/dist/services/newtonChat.js.map +1 -1
- 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/dist/services/school.d.ts +142 -15
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +308 -54
- package/dist/services/school.js.map +1 -1
- package/dist/services/tag.d.ts +71 -0
- package/dist/services/tag.d.ts.map +1 -0
- package/dist/services/tag.js +178 -0
- package/dist/services/tag.js.map +1 -0
- package/dist/services/worksheet.d.ts +1 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/utils/aiUser.d.ts.map +1 -1
- package/dist/utils/aiUser.js +2 -3
- package/dist/utils/aiUser.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +184 -0
- package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +93 -0
- package/prisma/schema.prisma +193 -35
- package/src/middleware/security.ts +1 -1
- package/src/models/access.ts +8 -24
- package/src/models/announcement.ts +0 -1
- package/src/models/auth.ts +0 -1
- package/src/models/class.ts +13 -0
- package/src/models/cloudStorage.ts +327 -0
- package/src/models/comment.ts +0 -4
- package/src/models/department.ts +159 -0
- package/src/models/event.ts +0 -3
- package/src/models/file.ts +2 -40
- package/src/models/message.ts +19 -5
- package/src/models/school.ts +26 -0
- package/src/models/tag.ts +140 -0
- package/src/routers/_app.ts +6 -0
- package/src/routers/announcement.ts +1 -3
- package/src/routers/assignment.ts +0 -1
- package/src/routers/class.ts +3 -0
- package/src/routers/department.ts +174 -0
- package/src/routers/file.ts +29 -26
- package/src/routers/folder.ts +127 -118
- package/src/routers/permissions.ts +71 -0
- package/src/routers/school.ts +83 -0
- package/src/routers/tag.ts +78 -0
- package/src/services/class.ts +109 -4
- package/src/services/department.ts +478 -0
- package/src/services/file.ts +172 -43
- package/src/services/folder.ts +399 -237
- package/src/services/labChat.ts +2 -1
- package/src/services/message.ts +5 -21
- package/src/services/newtonChat.ts +2 -1
- package/src/services/permissions.ts +778 -0
- package/src/services/school.ts +369 -57
- package/src/services/tag.ts +229 -0
- package/src/utils/aiUser.ts +0 -1
- package/tests/unit/services/permissions.test.ts +463 -0
package/dist/services/event.d.ts
CHANGED
|
@@ -7,7 +7,6 @@ export declare function getEvent(userId: string, id: string): Promise<{
|
|
|
7
7
|
email: string;
|
|
8
8
|
password: string;
|
|
9
9
|
verified: boolean;
|
|
10
|
-
role: import(".prisma/client").$Enums.UserRole;
|
|
11
10
|
schoolId: string | null;
|
|
12
11
|
} | null;
|
|
13
12
|
class: ({
|
|
@@ -21,6 +20,7 @@ export declare function getEvent(userId: string, id: string): Promise<{
|
|
|
21
20
|
} & {
|
|
22
21
|
name: string;
|
|
23
22
|
id: string;
|
|
23
|
+
description: string | null;
|
|
24
24
|
logoId: string | null;
|
|
25
25
|
subdomain: string | null;
|
|
26
26
|
createdAt: Date;
|
|
@@ -37,6 +37,7 @@ export declare function getEvent(userId: string, id: string): Promise<{
|
|
|
37
37
|
color: string | null;
|
|
38
38
|
section: string;
|
|
39
39
|
syllabus: string | null;
|
|
40
|
+
departmentId: string | null;
|
|
40
41
|
}) | null;
|
|
41
42
|
assignmentsAttached: {
|
|
42
43
|
type: import(".prisma/client").$Enums.AssignmentType;
|
|
@@ -142,8 +143,8 @@ export declare function attachAssignment(userId: string, eventId: string, assign
|
|
|
142
143
|
type: import(".prisma/client").$Enums.AssignmentType;
|
|
143
144
|
id: string;
|
|
144
145
|
createdAt: Date | null;
|
|
145
|
-
modifiedAt: Date | null;
|
|
146
146
|
classId: string;
|
|
147
|
+
modifiedAt: Date | null;
|
|
147
148
|
title: string;
|
|
148
149
|
instructions: string;
|
|
149
150
|
dueDate: Date;
|
|
@@ -184,8 +185,8 @@ export declare function detachAssignment(userId: string, eventId: string, assign
|
|
|
184
185
|
type: import(".prisma/client").$Enums.AssignmentType;
|
|
185
186
|
id: string;
|
|
186
187
|
createdAt: Date | null;
|
|
187
|
-
modifiedAt: Date | null;
|
|
188
188
|
classId: string;
|
|
189
|
+
modifiedAt: Date | null;
|
|
189
190
|
title: string;
|
|
190
191
|
instructions: string;
|
|
191
192
|
dueDate: Date;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event.d.ts","sourceRoot":"/","sources":["services/event.ts"],"names":[],"mappings":"AAmBA,6DAA6D;AAC7D,wBAAsB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM
|
|
1
|
+
{"version":3,"file":"event.d.ts","sourceRoot":"/","sources":["services/event.ts"],"names":[],"mappings":"AAmBA,6DAA6D;AAC7D,wBAAsB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAexD;AAED,wCAAwC;AACxC,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IACL,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;;;;;;;;;;;;;;;;GAwBF;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,MAAM,EACV,IAAI,EAAE;IACJ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;;;;;;;;;;;;GA2BF;AAED,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;;GAiBjE;AAuBD,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CrB;AAED,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkBrB;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;GAkBhB"}
|
package/dist/services/file.d.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Generate a signed URL for file download.
|
|
3
|
+
*
|
|
4
|
+
* Access resolution, in order — first match wins:
|
|
5
|
+
* 1. Owner of the file.
|
|
6
|
+
* 2. Member of the conversation the file is attached to (chat upload).
|
|
7
|
+
* 3. Contextual access via the file's class context: assignment / announcement
|
|
8
|
+
* / submission / annotations attached to a class the user teaches, learns
|
|
9
|
+
* in, or is a school admin for.
|
|
10
|
+
* 4. The central permissions resolver, which covers folder-scoped files —
|
|
11
|
+
* explicit Permission grants (USER / TAG / CLASS / DEPARTMENT / SCHOOL
|
|
12
|
+
* principals) plus scope defaults (school members read SCHOOL_STORAGE,
|
|
13
|
+
* class members read class folders, owners read personal folders).
|
|
14
|
+
*
|
|
15
|
+
* The resolver is invoked unconditionally as the last step, so files shared
|
|
16
|
+
* via the new Permission system reach this code path even when they have no
|
|
17
|
+
* matching domain context.
|
|
18
|
+
*/
|
|
2
19
|
export declare function getFileSignedUrl(userId: string, fileId: string): Promise<{
|
|
3
20
|
url: string;
|
|
4
21
|
}>;
|
|
@@ -13,6 +30,7 @@ export declare function moveFile(fileId: string, targetFolderId: string): Promis
|
|
|
13
30
|
name: string;
|
|
14
31
|
id: string;
|
|
15
32
|
userId: string | null;
|
|
33
|
+
folderId: string | null;
|
|
16
34
|
size: number | null;
|
|
17
35
|
uploadedAt: Date | null;
|
|
18
36
|
uploadStatus: import(".prisma/client").$Enums.UploadStatus;
|
|
@@ -29,7 +47,6 @@ export declare function moveFile(fileId: string, targetFolderId: string): Promis
|
|
|
29
47
|
submissionId: string | null;
|
|
30
48
|
annotationId: string | null;
|
|
31
49
|
classDraftId: string | null;
|
|
32
|
-
folderId: string | null;
|
|
33
50
|
conversationId: string | null;
|
|
34
51
|
messageId: string | null;
|
|
35
52
|
announcementId: string | null;
|
|
@@ -46,6 +63,7 @@ export declare function renameFile(fileId: string, newName: string): Promise<{
|
|
|
46
63
|
name: string;
|
|
47
64
|
id: string;
|
|
48
65
|
userId: string | null;
|
|
66
|
+
folderId: string | null;
|
|
49
67
|
size: number | null;
|
|
50
68
|
uploadedAt: Date | null;
|
|
51
69
|
uploadStatus: import(".prisma/client").$Enums.UploadStatus;
|
|
@@ -62,7 +80,6 @@ export declare function renameFile(fileId: string, newName: string): Promise<{
|
|
|
62
80
|
submissionId: string | null;
|
|
63
81
|
annotationId: string | null;
|
|
64
82
|
classDraftId: string | null;
|
|
65
|
-
folderId: string | null;
|
|
66
83
|
conversationId: string | null;
|
|
67
84
|
messageId: string | null;
|
|
68
85
|
announcementId: string | null;
|
|
@@ -71,4 +88,89 @@ export declare function renameFile(fileId: string, newName: string): Promise<{
|
|
|
71
88
|
export declare function deleteFileRecord(fileId: string, classId: string): Promise<{
|
|
72
89
|
success: boolean;
|
|
73
90
|
}>;
|
|
91
|
+
/**
|
|
92
|
+
* Scope-aware move: works for class, school, and personal storage. Source
|
|
93
|
+
* and target must live in the same scope (no cross-scope moves).
|
|
94
|
+
*/
|
|
95
|
+
export declare function moveFileScoped(userId: string, input: {
|
|
96
|
+
fileId: string;
|
|
97
|
+
targetFolderId: string;
|
|
98
|
+
}): Promise<{
|
|
99
|
+
user: {
|
|
100
|
+
id: string;
|
|
101
|
+
username: string;
|
|
102
|
+
} | null;
|
|
103
|
+
} & {
|
|
104
|
+
path: string;
|
|
105
|
+
type: string;
|
|
106
|
+
name: string;
|
|
107
|
+
id: string;
|
|
108
|
+
userId: string | null;
|
|
109
|
+
folderId: string | null;
|
|
110
|
+
size: number | null;
|
|
111
|
+
uploadedAt: Date | null;
|
|
112
|
+
uploadStatus: import(".prisma/client").$Enums.UploadStatus;
|
|
113
|
+
uploadUrl: string | null;
|
|
114
|
+
uploadExpiresAt: Date | null;
|
|
115
|
+
uploadSessionId: string | null;
|
|
116
|
+
uploadProgress: number | null;
|
|
117
|
+
uploadError: string | null;
|
|
118
|
+
uploadRetryCount: number;
|
|
119
|
+
isOrphaned: boolean;
|
|
120
|
+
cleanupAt: Date | null;
|
|
121
|
+
thumbnailId: string | null;
|
|
122
|
+
assignmentId: string | null;
|
|
123
|
+
submissionId: string | null;
|
|
124
|
+
annotationId: string | null;
|
|
125
|
+
classDraftId: string | null;
|
|
126
|
+
conversationId: string | null;
|
|
127
|
+
messageId: string | null;
|
|
128
|
+
announcementId: string | null;
|
|
129
|
+
schoolDevelopementProgramId: string | null;
|
|
130
|
+
}>;
|
|
131
|
+
/** Scope-aware rename. */
|
|
132
|
+
export declare function renameFileScoped(userId: string, input: {
|
|
133
|
+
fileId: string;
|
|
134
|
+
newName: string;
|
|
135
|
+
}): Promise<{
|
|
136
|
+
user: {
|
|
137
|
+
id: string;
|
|
138
|
+
username: string;
|
|
139
|
+
} | null;
|
|
140
|
+
} & {
|
|
141
|
+
path: string;
|
|
142
|
+
type: string;
|
|
143
|
+
name: string;
|
|
144
|
+
id: string;
|
|
145
|
+
userId: string | null;
|
|
146
|
+
folderId: string | null;
|
|
147
|
+
size: number | null;
|
|
148
|
+
uploadedAt: Date | null;
|
|
149
|
+
uploadStatus: import(".prisma/client").$Enums.UploadStatus;
|
|
150
|
+
uploadUrl: string | null;
|
|
151
|
+
uploadExpiresAt: Date | null;
|
|
152
|
+
uploadSessionId: string | null;
|
|
153
|
+
uploadProgress: number | null;
|
|
154
|
+
uploadError: string | null;
|
|
155
|
+
uploadRetryCount: number;
|
|
156
|
+
isOrphaned: boolean;
|
|
157
|
+
cleanupAt: Date | null;
|
|
158
|
+
thumbnailId: string | null;
|
|
159
|
+
assignmentId: string | null;
|
|
160
|
+
submissionId: string | null;
|
|
161
|
+
annotationId: string | null;
|
|
162
|
+
classDraftId: string | null;
|
|
163
|
+
conversationId: string | null;
|
|
164
|
+
messageId: string | null;
|
|
165
|
+
announcementId: string | null;
|
|
166
|
+
schoolDevelopementProgramId: string | null;
|
|
167
|
+
}>;
|
|
168
|
+
/**
|
|
169
|
+
* Generic delete that works across class / school / personal storage by
|
|
170
|
+
* deriving permissions from the parent folder's CloudScope (or owning user
|
|
171
|
+
* for unfiled files).
|
|
172
|
+
*/
|
|
173
|
+
export declare function deleteFileGeneric(userId: string, fileId: string): Promise<{
|
|
174
|
+
success: boolean;
|
|
175
|
+
}>;
|
|
74
176
|
//# sourceMappingURL=file.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.d.ts","sourceRoot":"/","sources":["services/file.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file.d.ts","sourceRoot":"/","sources":["services/file.ts"],"names":[],"mappings":"AA+CA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;;GAkFpE;AAED,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmBvB;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkBhB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;;GA6BrE;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsBlD;AAED,0BAA0B;AAC1B,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsB3C;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;;GA4BrE"}
|
package/dist/services/file.js
CHANGED
|
@@ -1,19 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* File service – signed URLs, move, rename, delete.
|
|
3
|
-
*
|
|
2
|
+
* File service – signed URLs, move, rename, delete.
|
|
3
|
+
*
|
|
4
|
+
* Two access concepts coexist here, and they should not be conflated:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Contextual access** — assignment, submission, announcement, and chat
|
|
7
|
+
* attachments. A teacher viewing a student's submission has access by
|
|
8
|
+
* virtue of teaching the class, not because anyone explicitly "shared"
|
|
9
|
+
* the file with them. These checks live inline in `getFileSignedUrl`
|
|
10
|
+
* below and predate the explicit-share system.
|
|
11
|
+
*
|
|
12
|
+
* 2. **Explicit-share access** — folder-scoped files in cloud storage.
|
|
13
|
+
* Permission flows through the central resolver in `permissions.ts`,
|
|
14
|
+
* which consults explicit `Permission` rows in addition to the
|
|
15
|
+
* scope-derived defaults. The cloud-storage helpers (`*Scoped` /
|
|
16
|
+
* `*Generic`) below all use this path.
|
|
17
|
+
*
|
|
18
|
+
* If you're tempted to refactor the contextual checks into the resolver,
|
|
19
|
+
* don't — they intentionally key off the file's domain context, not its
|
|
20
|
+
* filesystem location.
|
|
4
21
|
*/
|
|
5
22
|
|
|
6
|
-
!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]="
|
|
23
|
+
!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]="f5c84cd2-4bba-5d8c-80c8-a64d756df4c6")}catch(e){}}();
|
|
7
24
|
import { TRPCError } from "@trpc/server";
|
|
8
25
|
import { getSignedUrl, deleteFile } from "../lib/googleCloudStorage.js";
|
|
9
26
|
import { logger } from "../utils/logger.js";
|
|
10
27
|
import { findFileById, findFileWithFolder, findFileWithFolderAndThumbnail, findFolderById, updateFileFolder, updateFileName, deleteFileById, } from "../models/file.js";
|
|
11
28
|
import { findConversationMembership } from "../models/message.js";
|
|
29
|
+
import { resolveFolderScope } from "../models/cloudStorage.js";
|
|
30
|
+
import { requireFolderAccess } from "./folder.js";
|
|
31
|
+
import { requireFileAccess } from "./permissions.js";
|
|
12
32
|
function isSchoolAdminForClass(classData, userId) {
|
|
13
|
-
return Boolean(classData?.school?.memberships?.some((membership) => membership.userId === userId)
|
|
14
|
-
classData?.school?.users?.some((admin) => admin.id === userId));
|
|
33
|
+
return Boolean(classData?.school?.memberships?.some((membership) => membership.userId === userId));
|
|
15
34
|
}
|
|
16
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Generate a signed URL for file download.
|
|
37
|
+
*
|
|
38
|
+
* Access resolution, in order — first match wins:
|
|
39
|
+
* 1. Owner of the file.
|
|
40
|
+
* 2. Member of the conversation the file is attached to (chat upload).
|
|
41
|
+
* 3. Contextual access via the file's class context: assignment / announcement
|
|
42
|
+
* / submission / annotations attached to a class the user teaches, learns
|
|
43
|
+
* in, or is a school admin for.
|
|
44
|
+
* 4. The central permissions resolver, which covers folder-scoped files —
|
|
45
|
+
* explicit Permission grants (USER / TAG / CLASS / DEPARTMENT / SCHOOL
|
|
46
|
+
* principals) plus scope defaults (school members read SCHOOL_STORAGE,
|
|
47
|
+
* class members read class folders, owners read personal folders).
|
|
48
|
+
*
|
|
49
|
+
* The resolver is invoked unconditionally as the last step, so files shared
|
|
50
|
+
* via the new Permission system reach this code path even when they have no
|
|
51
|
+
* matching domain context.
|
|
52
|
+
*/
|
|
17
53
|
export async function getFileSignedUrl(userId, fileId) {
|
|
18
54
|
const file = await findFileById(fileId);
|
|
19
55
|
if (!file) {
|
|
@@ -23,46 +59,53 @@ export async function getFileSignedUrl(userId, fileId) {
|
|
|
23
59
|
});
|
|
24
60
|
}
|
|
25
61
|
let hasAccess = false;
|
|
26
|
-
if (file.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
hasAccess =
|
|
32
|
-
}
|
|
33
|
-
if (file.
|
|
34
|
-
const
|
|
35
|
-
const isTeacher = ann.class.teachers.some((t) => t.id === userId);
|
|
36
|
-
const isStudent = ann.class.students.some((s) => s.id === userId);
|
|
37
|
-
const isSchoolAdmin = isSchoolAdminForClass(ann.class, userId);
|
|
38
|
-
hasAccess = hasAccess || isTeacher || isStudent || isSchoolAdmin;
|
|
39
|
-
}
|
|
40
|
-
if (file.submission?.assignment?.classId) {
|
|
62
|
+
if (file.userId === userId) {
|
|
63
|
+
hasAccess = true;
|
|
64
|
+
}
|
|
65
|
+
if (!hasAccess && file.conversationId) {
|
|
66
|
+
const membership = await findConversationMembership(file.conversationId, userId);
|
|
67
|
+
hasAccess = Boolean(membership);
|
|
68
|
+
}
|
|
69
|
+
if (!hasAccess && file.assignment?.class) {
|
|
70
|
+
const cls = file.assignment.class;
|
|
41
71
|
hasAccess =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
isSchoolAdminForClass(file.submission.assignment.class, userId);
|
|
72
|
+
cls.teachers.some((t) => t.id === userId) ||
|
|
73
|
+
cls.students.some((s) => s.id === userId) ||
|
|
74
|
+
isSchoolAdminForClass(cls, userId);
|
|
46
75
|
}
|
|
47
|
-
if (file.
|
|
76
|
+
if (!hasAccess && file.announcement?.class) {
|
|
77
|
+
const cls = file.announcement.class;
|
|
48
78
|
hasAccess =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
isSchoolAdminForClass(file.annotations.assignment.class, userId);
|
|
79
|
+
cls.teachers.some((t) => t.id === userId) ||
|
|
80
|
+
cls.students.some((s) => s.id === userId) ||
|
|
81
|
+
isSchoolAdminForClass(cls, userId);
|
|
53
82
|
}
|
|
54
|
-
if (file.
|
|
55
|
-
|
|
83
|
+
if (!hasAccess && file.submission?.assignment?.classId) {
|
|
84
|
+
const cls = file.submission.assignment.class;
|
|
85
|
+
hasAccess =
|
|
86
|
+
file.submission.studentId === userId ||
|
|
87
|
+
cls.teachers.some((t) => t.id === userId) ||
|
|
88
|
+
isSchoolAdminForClass(cls, userId);
|
|
56
89
|
}
|
|
57
|
-
if (file.
|
|
58
|
-
const
|
|
59
|
-
hasAccess =
|
|
90
|
+
if (!hasAccess && file.annotations?.assignment?.classId) {
|
|
91
|
+
const cls = file.annotations.assignment.class;
|
|
92
|
+
hasAccess =
|
|
93
|
+
file.annotations.studentId === userId ||
|
|
94
|
+
cls.teachers.some((t) => t.id === userId) ||
|
|
95
|
+
isSchoolAdminForClass(cls, userId);
|
|
60
96
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
97
|
+
// Final fallback: the explicit-share + scope-default resolver. Covers
|
|
98
|
+
// folder-scoped files shared via Permission grants (incl. SCHOOL/CLASS/TAG
|
|
99
|
+
// principals) and any institution member reading SCHOOL_STORAGE files.
|
|
100
|
+
// Safe to call for unfiled files — the resolver returns owner-only there.
|
|
101
|
+
if (!hasAccess) {
|
|
102
|
+
try {
|
|
103
|
+
await requireFileAccess(userId, fileId, "READ");
|
|
104
|
+
hasAccess = true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// resolver denied — fall through to FORBIDDEN below.
|
|
108
|
+
}
|
|
66
109
|
}
|
|
67
110
|
if (!hasAccess) {
|
|
68
111
|
throw new TRPCError({
|
|
@@ -143,5 +186,88 @@ export async function deleteFileRecord(fileId, classId) {
|
|
|
143
186
|
await deleteFileById(fileId);
|
|
144
187
|
return { success: true };
|
|
145
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Scope-aware move: works for class, school, and personal storage. Source
|
|
191
|
+
* and target must live in the same scope (no cross-scope moves).
|
|
192
|
+
*/
|
|
193
|
+
export async function moveFileScoped(userId, input) {
|
|
194
|
+
const file = await findFileWithFolder(input.fileId);
|
|
195
|
+
if (!file?.folderId) {
|
|
196
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "File not found" });
|
|
197
|
+
}
|
|
198
|
+
const [sourceScope, targetScope] = await Promise.all([
|
|
199
|
+
resolveFolderScope(file.folderId),
|
|
200
|
+
resolveFolderScope(input.targetFolderId),
|
|
201
|
+
]);
|
|
202
|
+
if (!sourceScope || !targetScope) {
|
|
203
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "Folder not found" });
|
|
204
|
+
}
|
|
205
|
+
if (JSON.stringify(sourceScope) !== JSON.stringify(targetScope)) {
|
|
206
|
+
throw new TRPCError({
|
|
207
|
+
code: "BAD_REQUEST",
|
|
208
|
+
message: "Cannot move files between scopes",
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
await requireFolderAccess(userId, file.folderId, "WRITE");
|
|
212
|
+
await requireFolderAccess(userId, input.targetFolderId, "WRITE");
|
|
213
|
+
return updateFileFolder(input.fileId, input.targetFolderId);
|
|
214
|
+
}
|
|
215
|
+
/** Scope-aware rename. */
|
|
216
|
+
export async function renameFileScoped(userId, input) {
|
|
217
|
+
const trimmed = input.newName.trim();
|
|
218
|
+
if (!trimmed) {
|
|
219
|
+
throw new TRPCError({
|
|
220
|
+
code: "BAD_REQUEST",
|
|
221
|
+
message: "File name cannot be empty",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const file = await findFileWithFolder(input.fileId);
|
|
225
|
+
if (!file) {
|
|
226
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "File not found" });
|
|
227
|
+
}
|
|
228
|
+
if (file.folderId) {
|
|
229
|
+
await requireFolderAccess(userId, file.folderId, "WRITE");
|
|
230
|
+
}
|
|
231
|
+
else if (file.userId !== userId) {
|
|
232
|
+
throw new TRPCError({
|
|
233
|
+
code: "FORBIDDEN",
|
|
234
|
+
message: "You don't own this file",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return updateFileName(input.fileId, trimmed);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Generic delete that works across class / school / personal storage by
|
|
241
|
+
* deriving permissions from the parent folder's CloudScope (or owning user
|
|
242
|
+
* for unfiled files).
|
|
243
|
+
*/
|
|
244
|
+
export async function deleteFileGeneric(userId, fileId) {
|
|
245
|
+
const file = await findFileWithFolderAndThumbnail(fileId);
|
|
246
|
+
if (!file) {
|
|
247
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "File not found" });
|
|
248
|
+
}
|
|
249
|
+
if (file.folderId) {
|
|
250
|
+
await requireFolderAccess(userId, file.folderId, "WRITE");
|
|
251
|
+
}
|
|
252
|
+
else if (file.userId !== userId) {
|
|
253
|
+
throw new TRPCError({
|
|
254
|
+
code: "FORBIDDEN",
|
|
255
|
+
message: "You don't own this file",
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
if (file.uploadStatus === "COMPLETED") {
|
|
260
|
+
await deleteFile(file.path);
|
|
261
|
+
if (file.thumbnail) {
|
|
262
|
+
await deleteFile(file.thumbnail.path);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
logger.warn(`Failed to delete file ${file.path}:`, error);
|
|
268
|
+
}
|
|
269
|
+
await deleteFileById(fileId);
|
|
270
|
+
return { success: true };
|
|
271
|
+
}
|
|
146
272
|
//# sourceMappingURL=file.js.map
|
|
147
|
-
//# debugId=
|
|
273
|
+
//# debugId=f5c84cd2-4bba-5d8c-80c8-a64d756df4c6
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.js","sources":["services/file.ts"],"sourceRoot":"/","sourcesContent":["/**\n * File service – signed URLs, move, rename, delete. Handles permission checks\n * for assignment, submission, announcement, folder, and user-owned files.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { getSignedUrl, deleteFile } from \"../lib/googleCloudStorage.js\";\nimport { logger } from \"../utils/logger.js\";\nimport {\n findFileById,\n findFileWithFolder,\n findFileWithFolderAndThumbnail,\n findFolderById,\n updateFileFolder,\n updateFileName,\n deleteFileById,\n} from \"../models/file.js\";\nimport { findConversationMembership } from \"../models/message.js\";\n\nfunction isSchoolAdminForClass(classData: any, userId: string) {\n return Boolean(\n classData?.school?.memberships?.some(\n (membership: { userId: string }) => membership.userId === userId\n ) ||\n classData?.school?.users?.some((admin: { id: string }) => admin.id === userId)\n );\n}\n\n/** Generate a signed URL for file download. Validates user has access. */\nexport async function getFileSignedUrl(userId: string, fileId: string) {\n const file = await findFileById(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File does not exist\",\n });\n }\n\n let hasAccess = false;\n\n if (file.assignment?.class) {\n const isTeacher = file.assignment.class.teachers.some(\n (t) => t.id === userId\n );\n const isStudent = file.assignment.class.students.some(\n (s) => s.id === userId\n );\n const isSchoolAdmin = isSchoolAdminForClass(file.assignment.class, userId);\n logger.info(\n `Assignment file access check - userId: ${userId}, isTeacher: ${isTeacher}, isStudent: ${isStudent}, isSchoolAdmin: ${isSchoolAdmin}`\n );\n hasAccess = isTeacher || isStudent || isSchoolAdmin;\n }\n\n if ((file as any).announcement?.class) {\n const ann = (file as any).announcement;\n const isTeacher = ann.class.teachers.some((t: any) => t.id === userId);\n const isStudent = ann.class.students.some((s: any) => s.id === userId);\n const isSchoolAdmin = isSchoolAdminForClass(ann.class, userId);\n hasAccess = hasAccess || isTeacher || isStudent || isSchoolAdmin;\n }\n\n if (file.submission?.assignment?.classId) {\n hasAccess =\n hasAccess ||\n file.submission.studentId === userId ||\n file.submission.assignment.class.teachers.some((t) => t.id === userId) ||\n isSchoolAdminForClass(file.submission.assignment.class, userId);\n }\n\n if (file.annotations?.assignment?.classId) {\n hasAccess =\n hasAccess ||\n file.annotations.studentId === userId ||\n file.annotations.assignment.class.teachers.some((t) => t.id === userId) ||\n isSchoolAdminForClass(file.annotations.assignment.class, userId);\n }\n\n if (file.userId === userId) {\n hasAccess = true;\n }\n\n if (file.conversationId) {\n const membership = await findConversationMembership(file.conversationId, userId);\n hasAccess = hasAccess || Boolean(membership);\n }\n\n if (file.folder?.class) {\n const isTeacher = file.folder.class.teachers.some((t) => t.id === userId);\n const isStudent = file.folder.class.students.some((s) => s.id === userId);\n const isSchoolAdmin = isSchoolAdminForClass(file.folder.class, userId);\n hasAccess = hasAccess || isTeacher || isStudent || isSchoolAdmin;\n }\n\n if (!hasAccess) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You do not have access to this file\",\n });\n }\n\n try {\n const signedUrl = await getSignedUrl(file.path);\n return { url: signedUrl };\n } catch (error) {\n logger.error(\"Error generating signed URL:\", error as Record<string, unknown>);\n throw new TRPCError({\n code: \"INTERNAL_SERVER_ERROR\",\n message: \"Failed to generate download URL\",\n });\n }\n}\n\nexport async function moveFile(\n fileId: string,\n targetFolderId: string\n) {\n const file = await findFileWithFolder(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File not found\",\n });\n }\n\n const targetFolder = await findFolderById(targetFolderId);\n if (!targetFolder) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Target folder not found\",\n });\n }\n\n return updateFileFolder(fileId, targetFolderId);\n}\n\nexport async function renameFile(\n fileId: string,\n newName: string\n) {\n const file = await findFileWithFolder(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File not found\",\n });\n }\n\n if (!newName.trim()) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"File name cannot be empty\",\n });\n }\n\n return updateFileName(fileId, newName.trim());\n}\n\nexport async function deleteFileRecord(fileId: string, classId: string) {\n const file = await findFileWithFolderAndThumbnail(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File not found\",\n });\n }\n\n if (file.folder?.classId !== classId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"File does not belong to this class\",\n });\n }\n\n try {\n if (file.uploadStatus === \"COMPLETED\") {\n await deleteFile(file.path);\n if (file.thumbnail) {\n await deleteFile(file.thumbnail.path);\n }\n }\n } catch (error) {\n logger.warn(`Failed to delete file ${file.path}:`, error as Record<string, unknown>);\n }\n\n await deleteFileById(fileId);\n return { success: true };\n}\n"],"names":[],"mappings":"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","debug_id":"4ce26498-ba77-5a67-910b-428e64d7047b"}
|
|
1
|
+
{"version":3,"file":"file.js","sources":["services/file.ts"],"sourceRoot":"/","sourcesContent":["/**\n * File service – signed URLs, move, rename, delete.\n *\n * Two access concepts coexist here, and they should not be conflated:\n *\n * 1. **Contextual access** — assignment, submission, announcement, and chat\n * attachments. A teacher viewing a student's submission has access by\n * virtue of teaching the class, not because anyone explicitly \"shared\"\n * the file with them. These checks live inline in `getFileSignedUrl`\n * below and predate the explicit-share system.\n *\n * 2. **Explicit-share access** — folder-scoped files in cloud storage.\n * Permission flows through the central resolver in `permissions.ts`,\n * which consults explicit `Permission` rows in addition to the\n * scope-derived defaults. The cloud-storage helpers (`*Scoped` /\n * `*Generic`) below all use this path.\n *\n * If you're tempted to refactor the contextual checks into the resolver,\n * don't — they intentionally key off the file's domain context, not its\n * filesystem location.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { getSignedUrl, deleteFile } from \"../lib/googleCloudStorage.js\";\nimport { logger } from \"../utils/logger.js\";\nimport {\n findFileById,\n findFileWithFolder,\n findFileWithFolderAndThumbnail,\n findFolderById,\n updateFileFolder,\n updateFileName,\n deleteFileById,\n} from \"../models/file.js\";\nimport { findConversationMembership } from \"../models/message.js\";\nimport { prisma } from \"../lib/prisma.js\";\nimport { resolveFolderScope } from \"../models/cloudStorage.js\";\nimport { requireFolderAccess } from \"./folder.js\";\nimport { requireFileAccess } from \"./permissions.js\";\n\nfunction isSchoolAdminForClass(classData: any, userId: string) {\n return Boolean(\n classData?.school?.memberships?.some(\n (membership: { userId: string }) => membership.userId === userId\n )\n );\n}\n\n/**\n * Generate a signed URL for file download.\n *\n * Access resolution, in order — first match wins:\n * 1. Owner of the file.\n * 2. Member of the conversation the file is attached to (chat upload).\n * 3. Contextual access via the file's class context: assignment / announcement\n * / submission / annotations attached to a class the user teaches, learns\n * in, or is a school admin for.\n * 4. The central permissions resolver, which covers folder-scoped files —\n * explicit Permission grants (USER / TAG / CLASS / DEPARTMENT / SCHOOL\n * principals) plus scope defaults (school members read SCHOOL_STORAGE,\n * class members read class folders, owners read personal folders).\n *\n * The resolver is invoked unconditionally as the last step, so files shared\n * via the new Permission system reach this code path even when they have no\n * matching domain context.\n */\nexport async function getFileSignedUrl(userId: string, fileId: string) {\n const file = await findFileById(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File does not exist\",\n });\n }\n\n let hasAccess = false;\n\n if (file.userId === userId) {\n hasAccess = true;\n }\n\n if (!hasAccess && file.conversationId) {\n const membership = await findConversationMembership(file.conversationId, userId);\n hasAccess = Boolean(membership);\n }\n\n if (!hasAccess && file.assignment?.class) {\n const cls = file.assignment.class;\n hasAccess =\n cls.teachers.some((t) => t.id === userId) ||\n cls.students.some((s) => s.id === userId) ||\n isSchoolAdminForClass(cls, userId);\n }\n\n if (!hasAccess && (file as any).announcement?.class) {\n const cls = (file as any).announcement.class;\n hasAccess =\n cls.teachers.some((t: { id: string }) => t.id === userId) ||\n cls.students.some((s: { id: string }) => s.id === userId) ||\n isSchoolAdminForClass(cls, userId);\n }\n\n if (!hasAccess && file.submission?.assignment?.classId) {\n const cls = file.submission.assignment.class;\n hasAccess =\n file.submission.studentId === userId ||\n cls.teachers.some((t) => t.id === userId) ||\n isSchoolAdminForClass(cls, userId);\n }\n\n if (!hasAccess && file.annotations?.assignment?.classId) {\n const cls = file.annotations.assignment.class;\n hasAccess =\n file.annotations.studentId === userId ||\n cls.teachers.some((t) => t.id === userId) ||\n isSchoolAdminForClass(cls, userId);\n }\n\n // Final fallback: the explicit-share + scope-default resolver. Covers\n // folder-scoped files shared via Permission grants (incl. SCHOOL/CLASS/TAG\n // principals) and any institution member reading SCHOOL_STORAGE files.\n // Safe to call for unfiled files — the resolver returns owner-only there.\n if (!hasAccess) {\n try {\n await requireFileAccess(userId, fileId, \"READ\");\n hasAccess = true;\n } catch {\n // resolver denied — fall through to FORBIDDEN below.\n }\n }\n\n if (!hasAccess) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You do not have access to this file\",\n });\n }\n\n try {\n const signedUrl = await getSignedUrl(file.path);\n return { url: signedUrl };\n } catch (error) {\n logger.error(\"Error generating signed URL:\", error as Record<string, unknown>);\n throw new TRPCError({\n code: \"INTERNAL_SERVER_ERROR\",\n message: \"Failed to generate download URL\",\n });\n }\n}\n\nexport async function moveFile(\n fileId: string,\n targetFolderId: string\n) {\n const file = await findFileWithFolder(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File not found\",\n });\n }\n\n const targetFolder = await findFolderById(targetFolderId);\n if (!targetFolder) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Target folder not found\",\n });\n }\n\n return updateFileFolder(fileId, targetFolderId);\n}\n\nexport async function renameFile(\n fileId: string,\n newName: string\n) {\n const file = await findFileWithFolder(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File not found\",\n });\n }\n\n if (!newName.trim()) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"File name cannot be empty\",\n });\n }\n\n return updateFileName(fileId, newName.trim());\n}\n\nexport async function deleteFileRecord(fileId: string, classId: string) {\n const file = await findFileWithFolderAndThumbnail(fileId);\n if (!file) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"File not found\",\n });\n }\n\n if (file.folder?.classId !== classId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"File does not belong to this class\",\n });\n }\n\n try {\n if (file.uploadStatus === \"COMPLETED\") {\n await deleteFile(file.path);\n if (file.thumbnail) {\n await deleteFile(file.thumbnail.path);\n }\n }\n } catch (error) {\n logger.warn(`Failed to delete file ${file.path}:`, error as Record<string, unknown>);\n }\n\n await deleteFileById(fileId);\n return { success: true };\n}\n\n/**\n * Scope-aware move: works for class, school, and personal storage. Source\n * and target must live in the same scope (no cross-scope moves).\n */\nexport async function moveFileScoped(\n userId: string,\n input: { fileId: string; targetFolderId: string }\n) {\n const file = await findFileWithFolder(input.fileId);\n if (!file?.folderId) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"File not found\" });\n }\n const [sourceScope, targetScope] = await Promise.all([\n resolveFolderScope(file.folderId),\n resolveFolderScope(input.targetFolderId),\n ]);\n if (!sourceScope || !targetScope) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"Folder not found\" });\n }\n if (JSON.stringify(sourceScope) !== JSON.stringify(targetScope)) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"Cannot move files between scopes\",\n });\n }\n await requireFolderAccess(userId, file.folderId, \"WRITE\");\n await requireFolderAccess(userId, input.targetFolderId, \"WRITE\");\n return updateFileFolder(input.fileId, input.targetFolderId);\n}\n\n/** Scope-aware rename. */\nexport async function renameFileScoped(\n userId: string,\n input: { fileId: string; newName: string }\n) {\n const trimmed = input.newName.trim();\n if (!trimmed) {\n throw new TRPCError({\n code: \"BAD_REQUEST\",\n message: \"File name cannot be empty\",\n });\n }\n const file = await findFileWithFolder(input.fileId);\n if (!file) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"File not found\" });\n }\n if (file.folderId) {\n await requireFolderAccess(userId, file.folderId, \"WRITE\");\n } else if (file.userId !== userId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You don't own this file\",\n });\n }\n return updateFileName(input.fileId, trimmed);\n}\n\n/**\n * Generic delete that works across class / school / personal storage by\n * deriving permissions from the parent folder's CloudScope (or owning user\n * for unfiled files).\n */\nexport async function deleteFileGeneric(userId: string, fileId: string) {\n const file = await findFileWithFolderAndThumbnail(fileId);\n if (!file) {\n throw new TRPCError({ code: \"NOT_FOUND\", message: \"File not found\" });\n }\n if (file.folderId) {\n await requireFolderAccess(userId, file.folderId, \"WRITE\");\n } else if (file.userId !== userId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You don't own this file\",\n });\n }\n try {\n if (file.uploadStatus === \"COMPLETED\") {\n await deleteFile(file.path);\n if (file.thumbnail) {\n await deleteFile(file.thumbnail.path);\n }\n }\n } catch (error) {\n logger.warn(\n `Failed to delete file ${file.path}:`,\n error as Record<string, unknown>\n );\n }\n await deleteFileById(fileId);\n return { success: true };\n}\n"],"names":[],"mappings":"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","debug_id":"f5c84cd2-4bba-5d8c-80c8-a64d756df4c6"}
|