Skip to content

Commit 7b60031

Browse files
author
Liam Lloyd-Tucker
committed
Add GET /folder/{id}/share-links endpoint
Users should be able to see the share links they've created on any given folder, so this commit adds an endpoint to retrieve all a folder's share links.
1 parent b231bfe commit 7b60031

File tree

6 files changed

+210
-7
lines changed

6 files changed

+210
-7
lines changed

Diff for: packages/api/src/folder/controller/controller.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import {
99
extractUserEmailFromAuthToken,
1010
verifyUserAuthentication,
1111
} from "../../middleware";
12-
import { patchFolder, getFolders, getFolderChildren } from "../service";
12+
import {
13+
patchFolder,
14+
getFolders,
15+
getFolderChildren,
16+
getFolderShareLinks,
17+
} from "../service";
1318
import {
1419
validatePatchFolderRequest,
1520
validateFolderRequest,
@@ -19,6 +24,7 @@ import { isValidationError } from "../../validators/validator_util";
1924
import {
2025
validateOptionalAuthenticationValues,
2126
validatePaginationParameters,
27+
validateBodyFromAuthentication,
2228
} from "../../validators/shared";
2329

2430
export const folderController = Router();
@@ -99,3 +105,25 @@ folderController.get(
99105
}
100106
}
101107
);
108+
109+
folderController.get(
110+
"/:folderId/share_links",
111+
verifyUserAuthentication,
112+
async (req: Request, res: Response, next: NextFunction) => {
113+
try {
114+
validateFolderRequest(req.params);
115+
validateBodyFromAuthentication(req.body);
116+
const shareLinks = await getFolderShareLinks(
117+
req.body.emailFromAuthToken,
118+
req.params.folderId
119+
);
120+
res.status(200).send({ items: shareLinks });
121+
} catch (err) {
122+
if (isValidationError(err)) {
123+
res.status(400).json({ error: err.message });
124+
return;
125+
}
126+
next(err);
127+
}
128+
}
129+
);

Diff for: packages/api/src/folder/controller/get_folder.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jest.mock("../../database");
1313
jest.mock("../../middleware");
1414
jest.mock("@stela/logger");
1515

16-
describe("GET /folder/{id}", () => {
16+
describe("GET /folder", () => {
1717
const agent = request(app);
1818

1919
beforeEach(async () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type { NextFunction, Request } from "express";
2+
import request from "supertest";
3+
import { logger } from "@stela/logger";
4+
import createError from "http-errors";
5+
import { app } from "../../app";
6+
import { db } from "../../database";
7+
import { verifyUserAuthentication } from "../../middleware";
8+
import type { ShareLink } from "../../share_link/models";
9+
import { loadFixtures, clearDatabase } from "./utils_test";
10+
11+
jest.mock("../../database");
12+
jest.mock("../../middleware");
13+
jest.mock("@stela/logger");
14+
15+
describe("GET /folder/{id}/share_links", () => {
16+
const agent = request(app);
17+
18+
beforeEach(async () => {
19+
(verifyUserAuthentication as jest.Mock).mockImplementation(
20+
async (
21+
req: Request<
22+
unknown,
23+
unknown,
24+
{ userSubjectFromAuthToken?: string; emailFromAuthToken?: string }
25+
>,
26+
__,
27+
next: NextFunction
28+
) => {
29+
req.body.emailFromAuthToken = "[email protected]";
30+
req.body.userSubjectFromAuthToken =
31+
"b5461dc2-1eb0-450e-b710-fef7b2cafe1e";
32+
33+
next();
34+
}
35+
);
36+
await clearDatabase();
37+
await loadFixtures();
38+
});
39+
40+
afterEach(async () => {
41+
await clearDatabase();
42+
jest.restoreAllMocks();
43+
jest.clearAllMocks();
44+
});
45+
46+
test("expect to return share links for a folder", async () => {
47+
const response = await agent
48+
.get("/api/v2/folder/2/share_links")
49+
.expect(200);
50+
51+
const shareLinks = (response.body as { items: ShareLink[] }).items;
52+
expect(shareLinks.length).toEqual(3);
53+
54+
const shareLink = shareLinks.find((link) => link.id === "1");
55+
expect(shareLink?.id).toEqual("1");
56+
expect(shareLink?.itemId).toEqual("2");
57+
expect(shareLink?.itemType).toEqual("folder");
58+
expect(shareLink?.token).toEqual("c0f523e4-48d8-4c39-8cda-5e95161532e4");
59+
expect(shareLink?.permissionsLevel).toEqual("viewer");
60+
expect(shareLink?.accessRestrictions).toEqual("none");
61+
expect(shareLink?.maxUses).toEqual(null);
62+
expect(shareLink?.usesExpended).toEqual(null);
63+
expect(shareLink?.expirationTimestamp).toEqual(null);
64+
});
65+
66+
test("expect an empty list if folder doesn't exist", async () => {
67+
const response = await agent
68+
.get("/api/v2/folder/999/share_links")
69+
.expect(200);
70+
71+
const shareLinks = (response.body as { items: ShareLink[] }).items;
72+
expect(shareLinks.length).toEqual(0);
73+
});
74+
75+
test("expect empty list if user doesn't have access to the folder's share links", async () => {
76+
(verifyUserAuthentication as jest.Mock).mockImplementation(
77+
async (
78+
req: Request<
79+
unknown,
80+
unknown,
81+
{ userSubjectFromAuthToken?: string; emailFromAuthToken?: string }
82+
>,
83+
__,
84+
next: NextFunction
85+
) => {
86+
req.body.emailFromAuthToken = "[email protected]";
87+
req.body.userSubjectFromAuthToken =
88+
"b5461dc2-1eb0-450e-b710-fef7b2cafe1e";
89+
90+
next();
91+
}
92+
);
93+
const response = await agent
94+
.get("/api/v2/folder/2/share_links")
95+
.expect(200);
96+
97+
const shareLinks = (response.body as { items: ShareLink[] }).items;
98+
expect(shareLinks.length).toEqual(0);
99+
});
100+
101+
test("expect to log error and return 500 if database lookup fails", async () => {
102+
const testError = new Error("test error");
103+
jest.spyOn(db, "sql").mockImplementation(async () => {
104+
throw testError;
105+
});
106+
107+
await agent.get("/api/v2/folder/1/share_links").expect(500);
108+
expect(logger.error).toHaveBeenCalledWith(testError);
109+
});
110+
111+
test("expect 401 if not authenticated", async () => {
112+
(verifyUserAuthentication as jest.Mock).mockImplementation(
113+
(_, __, next: NextFunction) => {
114+
next(createError.Unauthorized("Invalid auth token"));
115+
}
116+
);
117+
await agent.get("/api/v2/folder/1/share_links").expect(401);
118+
});
119+
120+
test("expect 400 if the header values are missing", async () => {
121+
(verifyUserAuthentication as jest.Mock).mockImplementation(
122+
(_, __, next: NextFunction) => {
123+
next();
124+
}
125+
);
126+
await agent.get("/api/v2/folder/1/share_links").expect(400);
127+
});
128+
});
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,56 @@
11
INSERT INTO
22
shareby_url (
3+
shareby_urlid,
34
folder_linkid,
45
urltoken,
56
shareurl,
67
byaccountid,
78
byarchiveid,
89
unrestricted,
9-
expiresdt
10+
expiresdt,
11+
defaultaccessrole
1012
)
1113
VALUES (
14+
1,
1215
1,
1316
'c0f523e4-48d8-4c39-8cda-5e95161532e4',
1417
'https://local.permanent.org/share/c0f523e4-48d8-4c39-8cda-5e95161532e4',
1518
2,
1619
1,
1720
true,
18-
null
21+
null,
22+
'role.access.viewer'
1923
),
2024
(
25+
2,
2126
1,
2227
'7d6412af-5abe-4acb-808a-64e9ce3b7535',
2328
'https://local.permanent.org/share/7d6412af-5abe-4acb-808a-64e9ce3b7535',
2429
2,
2530
1,
2631
false,
27-
null
32+
null,
33+
'role.access.viewer'
2834
),
2935
(
36+
3,
3037
1,
3138
'9cc057f0-d3e8-41df-94d6-9b315b4921af',
3239
'https://local.permanent.org/share/9cc057f0-d3e8-41df-94d6-9b315b4921af',
3340
2,
3441
1,
3542
true,
36-
'2020-01-01'
43+
'2020-01-01',
44+
'role.access.viewer'
3745
),
3846
(
47+
4,
3948
3,
4049
'56f7c246-e4ec-41f3-b117-6df4c9377075',
4150
'https://local.permanent.org/share/56f7c246-e4ec-41f3-b117-6df4c9377075',
4251
2,
4352
1,
4453
true,
45-
null
54+
null,
55+
'role.access.viewer'
4656
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
SELECT shareby_url.shareby_urlid AS "id"
2+
FROM
3+
shareby_url
4+
INNER JOIN
5+
account ON shareby_url.byaccountid = account.accountid
6+
INNER JOIN
7+
folder_link ON shareby_url.folder_linkid = folder_link.folder_linkid
8+
WHERE
9+
folder_link.folderid = :folderId
10+
AND account.primaryemail = :email;

Diff for: packages/api/src/folder/service.ts

+27
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { requestFieldsToDatabaseFields } from "./helper";
2323
import { getFolderAccessRole, accessRoleLessThan } from "../access/permission";
2424
import { AccessRole } from "../access/models";
2525
import { getRecordById } from "../record/service";
26+
import { shareLinkService } from "../share_link/service";
27+
import type { ShareLink } from "../share_link/models";
2628

2729
export const prettifyFolderSortType = (
2830
sortType: FolderSortOrder
@@ -253,3 +255,28 @@ export const patchFolder = async (
253255
}
254256
return result.rows[0].folderId;
255257
};
258+
259+
export const getFolderShareLinks = async (
260+
email: string,
261+
folderId: string
262+
): Promise<ShareLink[]> => {
263+
const folderShareLinkIds = await db
264+
.sql<{ id: string }>("folder.queries.get_folder_share_links", {
265+
email,
266+
folderId,
267+
})
268+
.catch((err) => {
269+
logger.error(err);
270+
throw new createError.InternalServerError(
271+
"Failed to get folder share links"
272+
);
273+
});
274+
275+
const shareLinkIds = folderShareLinkIds.rows.map((row) => row.id);
276+
const shareLinks = await shareLinkService.getShareLinks(
277+
email,
278+
[],
279+
shareLinkIds
280+
);
281+
return shareLinks;
282+
};

0 commit comments

Comments
 (0)