Skip to content

Commit 164c434

Browse files
authored
- block file link generation when it contains reported files (#174)
- set default folder from api - fix builds
1 parent 2f6d447 commit 164c434

14 files changed

Lines changed: 217 additions & 46 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ Install the package managers `bun` and `pnpm` globally. You can do this using np
2626
```sh
2727
npm install -g bun
2828
npm install -g pnpm
29+
# This step is optional if you don't have lerna installed globally but it's easier to run commands that use it
30+
pnpm install -g lerna
2931
```
3032

3133
Or alternatively
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Container" ADD COLUMN "isDefault" BOOLEAN NOT NULL DEFAULT false;

packages/send/backend/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ model Container {
190190
children Container[] @relation("Nesting")
191191
192192
tags Tag[]
193+
isDefault Boolean @default(false) // Whether this is the user's default folder
193194
}
194195

195196
enum ItemType {

packages/send/backend/src/models/containers.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export async function getContainerWithAncestors(id: string) {
105105
}
106106

107107
export async function getContainerWithoutAncestors(userId: string) {
108-
const topLevelContainers = prisma.container.findFirst({
108+
const topLevelContainers = prisma.container.findMany({
109109
where: {
110110
parent: { is: null },
111111
owner: { id: userId },
@@ -114,6 +114,31 @@ export async function getContainerWithoutAncestors(userId: string) {
114114
return topLevelContainers;
115115
}
116116

117+
export async function setContainerAsDefault(
118+
container: string,
119+
ownerId: string
120+
) {
121+
return await prisma.container.update({
122+
where: {
123+
id: container,
124+
ownerId,
125+
},
126+
data: {
127+
isDefault: true,
128+
updatedAt: new Date(),
129+
},
130+
});
131+
}
132+
133+
export async function getDefaultContainerForOwner(ownerId: string) {
134+
return await prisma.container.findFirst({
135+
where: {
136+
ownerId,
137+
isDefault: true,
138+
},
139+
});
140+
}
141+
117142
export async function getAccessLinksForContainer(containerId: string) {
118143
const shares = await fromPrismaV2(prisma.share.findMany, {
119144
where: {

packages/send/backend/src/models/sharing.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,3 +833,21 @@ export async function deleteAccessLink(linkId: string) {
833833
export async function burnEphemeralConversation(containerId: string) {
834834
return await burnFolder(containerId);
835835
}
836+
837+
export async function checkIfAccessLinkCanBeCreated(containerId: string) {
838+
const reportedUploads = await prisma.container.findUnique({
839+
where: { id: containerId },
840+
select: {
841+
items: {
842+
where: {
843+
upload: {
844+
is: {
845+
reported: true,
846+
},
847+
},
848+
},
849+
},
850+
},
851+
});
852+
return reportedUploads?.items.length === 0;
853+
}

packages/send/backend/src/routes/sharing.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
acceptAccessLink,
55
acceptInvitation,
66
burnEphemeralConversation,
7+
checkIfAccessLinkCanBeCreated,
78
createAccessLink,
89
createInvitationFromAccessLink,
910
getAccessLinkChallenge,
@@ -23,7 +24,10 @@ import {
2324

2425
import { getDataFromAuthenticatedRequest } from '@send-backend/auth/client';
2526
import { useMetrics } from '@send-backend/metrics';
26-
import { addExpiryToContainer } from '@send-backend/utils';
27+
import {
28+
addExpiryToContainer,
29+
formatAccessLinkWithPasswordHash,
30+
} from '@send-backend/utils';
2731
import {
2832
getGroupMemberPermissions,
2933
requireAdminPermission,
@@ -95,6 +99,17 @@ router.post(
9599
if (req.body.permission) {
96100
permission = req.body.permission;
97101
}
102+
103+
// check if link can be created
104+
const canCreateLink = await checkIfAccessLinkCanBeCreated(containerId);
105+
106+
if (!canCreateLink) {
107+
return res.status(403).json({
108+
message:
109+
'Cannot create access link for this container because it contains files that have been reported for abuse.',
110+
});
111+
}
112+
98113
const accessLink = await createAccessLink(
99114
containerId,
100115
senderId,
@@ -126,6 +141,20 @@ router.post(
126141
})
127142
);
128143

144+
router.get(
145+
'/:containerId/canCreateAccessLink',
146+
requireJWT,
147+
getGroupMemberPermissions,
148+
requireSharePermission,
149+
wrapAsyncHandler(async (req, res) => {
150+
const { containerId } = req.params;
151+
const canCreateLink = await checkIfAccessLinkCanBeCreated(containerId);
152+
return res.status(200).json({
153+
canCreateLink,
154+
});
155+
})
156+
);
157+
129158
/**
130159
* @openapi
131160
* /api/sharing/accept/{invitationId}:
@@ -307,10 +336,12 @@ router.get(
307336

308337
if (type === 'file') {
309338
const result = await getAccessLinksByUploadIdAndWrappedKey(uploadId);
310-
return res.status(200).json(result);
339+
const formattedLinks = formatAccessLinkWithPasswordHash(result);
340+
return res.status(200).json(formattedLinks);
311341
}
312342
const result = await getAccessLinksByUploadId(uploadId);
313-
return res.status(200).json(result);
343+
const formattedLinks = formatAccessLinkWithPasswordHash(result);
344+
return res.status(200).json(formattedLinks);
314345
})
315346
);
316347

packages/send/backend/src/trpc/containers.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
import {
77
getAccessLinksForContainer as getAccessLinks,
88
getContainerWithoutAncestors,
9+
getDefaultContainerForOwner,
10+
setContainerAsDefault,
911
} from '@send-backend/models/containers';
1012
import { getAllUserGroupContainers } from '@send-backend/models/users';
1113
import { addExpiryToContainer } from '@send-backend/utils';
@@ -211,9 +213,31 @@ export const containersRouter = router({
211213
id: '',
212214
};
213215

216+
// get the default folder for the user where the isDefault flag is true
217+
const defaultFolder = await getDefaultContainerForOwner(id);
218+
if (defaultFolder) {
219+
response.id = defaultFolder.id;
220+
return response;
221+
}
222+
223+
// If we don't have a default folder, we need to get all the folders for the user and set oldest one as default
214224
try {
215-
const containers = await getContainerWithoutAncestors(id);
216-
response.id = containers.id;
225+
const containersWithoutAncestors =
226+
await getContainerWithoutAncestors(id);
227+
228+
// If the user has no folders at all, we just return an empty response
229+
if (!containersWithoutAncestors.length) {
230+
return response;
231+
}
232+
233+
// Make sure we tag the first container (by creation date) as default so we don't have issues later
234+
const sortedContainers = containersWithoutAncestors.sort((a, b) =>
235+
a.createdAt > b.createdAt ? 1 : -1
236+
);
237+
238+
await setContainerAsDefault(sortedContainers?.[0]?.id, id);
239+
response.id = sortedContainers?.[0]?.id;
240+
217241
return response;
218242
} catch {
219243
throw new TRPCError({

packages/send/backend/src/trpc/users.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,6 @@ export const usersRouter = router({
181181
)
182182
.subscription(async function* (opts) {
183183
if (!opts.ctx?.user?.id) {
184-
console.error('Verification can only be done by logged for users');
185184
return;
186185
}
187186
// listen for new events
@@ -203,7 +202,6 @@ export const usersRouter = router({
203202
)
204203
.subscription(async function* (opts) {
205204
if (!opts.ctx?.user?.id) {
206-
console.error('Verification can only be done by logged for users');
207205
return;
208206
}
209207
// listen for new events
@@ -230,7 +228,6 @@ export const usersRouter = router({
230228
)
231229
.subscription(async function* (opts) {
232230
if (!opts.ctx?.user?.id) {
233-
console.error('Verification can only be done by logged for users');
234231
return;
235232
}
236233
// listen for new events

packages/send/backend/src/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,17 @@ export const addExpiryToContainer = ({ createdAt, ...upload }: Upload) => {
9393
expired: daysToExpiry <= 0,
9494
};
9595
};
96+
type Shares = {
97+
id: string;
98+
passwordHash: string;
99+
expiryDate: Date;
100+
locked: boolean;
101+
};
102+
export const formatAccessLinkWithPasswordHash = (shares: Partial<Shares>[]) => {
103+
return shares.map((link) => {
104+
// If there password hash is present, we add it to the id so that the full shareable link can be shown to the user
105+
return link.passwordHash
106+
? { ...link, id: link.id + `#${link.passwordHash}` }
107+
: link;
108+
});
109+
};

packages/send/frontend/src/apps/send/components/CreateAccessLink.vue

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const tooltipText = ref('Copied to clipboard');
2424
const clipboard = useClipboard();
2525
const accessUrlInput = ref<HTMLInputElement | null>(null);
2626
const isLoading = ref(false);
27+
const errorMessage = ref('');
2728
2829
const { mutate } = useMutation({
2930
mutationKey: ['getAccessLink'],
@@ -50,31 +51,39 @@ function copyToClipboard(url: string) {
5051
5152
async function newAccessLink() {
5253
isLoading.value = true;
53-
const url = await sharingStore.createAccessLink(
54-
props.folderId,
55-
password.value,
56-
expiration.value
57-
);
58-
59-
if (!url) {
54+
try {
55+
const url = await sharingStore.createAccessLink(
56+
props.folderId,
57+
password.value,
58+
expiration.value
59+
);
60+
61+
if (!url) {
62+
emit('createAccessLinkError');
63+
errorMessage.value = 'Failed to create access link. Please try again.';
64+
isLoading.value = false;
65+
return;
66+
}
67+
68+
accessUrl.value = url;
69+
70+
if (!password.value.length) {
71+
mutate();
72+
}
73+
74+
// Copy url to clipboard
75+
clipboard.copy(url);
76+
77+
// Focus the input
78+
accessUrlInput.value?.focus();
79+
80+
await refreshAccessLinks();
81+
isLoading.value = false;
82+
} catch (error) {
6083
emit('createAccessLinkError');
61-
return;
62-
}
63-
64-
accessUrl.value = url;
65-
66-
if (!password.value.length) {
67-
mutate();
84+
errorMessage.value = error;
85+
isLoading.value = false;
6886
}
69-
70-
// Copy url to clipboard
71-
clipboard.copy(url);
72-
73-
// Focus the input
74-
accessUrlInput.value?.focus();
75-
76-
await refreshAccessLinks();
77-
isLoading.value = false;
7887
}
7988
8089
watch(
@@ -84,6 +93,7 @@ watch(
8493
expiration.value = null;
8594
accessUrl.value = '';
8695
showPassword.value = false;
96+
errorMessage.value = '';
8797
}
8898
);
8999
</script>
@@ -102,7 +112,6 @@ watch(
102112
</label>
103113
<label class="form-label">
104114
<span class="label-text">Link Expires</span>
105-
<input v-model="expiration" type="datetime-local" />
106115
</label>
107116
<label class="form-label password-field">
108117
<span class="label-text">Password</span>
@@ -120,6 +129,12 @@ watch(
120129
</button>
121130
</label>
122131
</section>
132+
133+
<!-- Error message display -->
134+
<div v-if="errorMessage" class="error-message" data-testid="error-message">
135+
{{ errorMessage }}
136+
</div>
137+
123138
<Btn
124139
class="create-button"
125140
data-testid="create-share-link"
@@ -174,4 +189,14 @@ watch(
174189
.create-button {
175190
margin-bottom: 2rem;
176191
}
192+
193+
.error-message {
194+
background-color: #fee2e2;
195+
border: 1px solid #fecaca;
196+
color: #dc2626;
197+
padding: 0.75rem;
198+
border-radius: 0.375rem;
199+
font-size: 0.875rem;
200+
margin-bottom: 1rem;
201+
}
177202
</style>

0 commit comments

Comments
 (0)