Skip to content

Commit cac7a32

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 e32aec0 commit cac7a32

File tree

5 files changed

+196
-7
lines changed

5 files changed

+196
-7
lines changed

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

+117
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { NextFunction, Request } from "express";
22
import { logger } from "@stela/logger";
33
import request from "supertest";
44
import { when } from "jest-when";
5+
import createError from "http-errors";
56
import { app } from "../app";
67
import { db } from "../database";
78
import {
@@ -10,6 +11,7 @@ import {
1011
verifyUserAuthentication,
1112
} from "../middleware";
1213
import type { Folder } from "./models";
14+
import type { ShareLink } from "../share_link/models";
1315

1416
jest.mock("../database");
1517
jest.mock("../middleware");
@@ -676,3 +678,118 @@ describe("GET /folder/{id}", () => {
676678
await agent.get("/api/v2/folder?folderIds[]=2").expect(500);
677679
});
678680
});
681+
682+
describe("GET /folder/{id}/share_links", () => {
683+
const agent = request(app);
684+
685+
beforeEach(async () => {
686+
(verifyUserAuthentication as jest.Mock).mockImplementation(
687+
async (
688+
req: Request<
689+
unknown,
690+
unknown,
691+
{ userSubjectFromAuthToken?: string; emailFromAuthToken?: string }
692+
>,
693+
__,
694+
next: NextFunction
695+
) => {
696+
req.body.emailFromAuthToken = "[email protected]";
697+
req.body.userSubjectFromAuthToken =
698+
"b5461dc2-1eb0-450e-b710-fef7b2cafe1e";
699+
700+
next();
701+
}
702+
);
703+
await clearDatabase();
704+
await loadFixtures();
705+
});
706+
707+
afterEach(async () => {
708+
await clearDatabase();
709+
jest.restoreAllMocks();
710+
jest.clearAllMocks();
711+
});
712+
713+
test("expect to return share links for a folder", async () => {
714+
const response = await agent
715+
.get("/api/v2/folder/2/share_links")
716+
.expect(200);
717+
718+
const shareLinks = (response.body as { items: ShareLink[] }).items;
719+
expect(shareLinks.length).toEqual(3);
720+
721+
const shareLink = shareLinks.find((link) => link.id === "1");
722+
expect(shareLink?.id).toEqual("1");
723+
expect(shareLink?.itemId).toEqual("2");
724+
expect(shareLink?.itemType).toEqual("folder");
725+
expect(shareLink?.token).toEqual("c0f523e4-48d8-4c39-8cda-5e95161532e4");
726+
expect(shareLink?.permissionsLevel).toEqual("viewer");
727+
expect(shareLink?.accessRestrictions).toEqual("none");
728+
expect(shareLink?.maxUses).toEqual(null);
729+
expect(shareLink?.usesExpended).toEqual(null);
730+
expect(shareLink?.expirationTimestamp).toEqual(null);
731+
});
732+
733+
test("expect an empty list if folder doesn't exist", async () => {
734+
const response = await agent
735+
.get("/api/v2/folder/999/share_links")
736+
.expect(200);
737+
738+
const shareLinks = (response.body as { items: ShareLink[] }).items;
739+
expect(shareLinks.length).toEqual(0);
740+
});
741+
742+
test("expect empty list if user doesn't have access to the folder's share links", async () => {
743+
(verifyUserAuthentication as jest.Mock).mockImplementation(
744+
async (
745+
req: Request<
746+
unknown,
747+
unknown,
748+
{ userSubjectFromAuthToken?: string; emailFromAuthToken?: string }
749+
>,
750+
__,
751+
next: NextFunction
752+
) => {
753+
req.body.emailFromAuthToken = "[email protected]";
754+
req.body.userSubjectFromAuthToken =
755+
"b5461dc2-1eb0-450e-b710-fef7b2cafe1e";
756+
757+
next();
758+
}
759+
);
760+
const response = await agent
761+
.get("/api/v2/folder/2/share_links")
762+
.expect(200);
763+
764+
const shareLinks = (response.body as { items: ShareLink[] }).items;
765+
expect(shareLinks.length).toEqual(0);
766+
});
767+
768+
test("expect to log error and return 500 if database lookup fails", async () => {
769+
const testError = new Error("test error");
770+
jest.spyOn(db, "sql").mockImplementation(async () => {
771+
throw testError;
772+
});
773+
774+
await agent.get("/api/v2/folder/1/share_links").expect(500);
775+
expect(logger.error).toHaveBeenCalledWith(testError);
776+
});
777+
778+
test("expect 401 if not authenticated", async () => {
779+
(verifyUserAuthentication as jest.Mock).mockImplementation(
780+
(_, __, next: NextFunction) => {
781+
next(createError.Unauthorized("Invalid auth token"));
782+
}
783+
);
784+
await agent.get("/api/v2/folder/1/share_links").expect(401);
785+
});
786+
787+
test("expect 400 if the header values are missing", async () => {
788+
(verifyUserAuthentication as jest.Mock).mockImplementation(
789+
(_, __, next: NextFunction) => {
790+
next();
791+
}
792+
);
793+
await agent.get("/api/v2/folder/1/share_links").expect(400);
794+
});
795+
});

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

+27-2
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ import {
99
extractUserEmailFromAuthToken,
1010
verifyUserAuthentication,
1111
} from "../middleware";
12-
import { patchFolder, getFolders } from "./service";
12+
import { patchFolder, getFolders, getFolderShareLinks } from "./service";
1313
import {
1414
validatePatchFolderRequest,
1515
validateFolderRequest,
1616
validateGetFoldersQuery,
1717
} from "./validators";
1818
import { isValidationError } from "../validators/validator_util";
19-
import { validateOptionalAuthenticationValues } from "../validators/shared";
19+
import {
20+
validateOptionalAuthenticationValues,
21+
validateBodyFromAuthentication,
22+
} from "../validators/shared";
2023

2124
export const folderController = Router();
2225

@@ -69,3 +72,25 @@ folderController.get(
6972
}
7073
}
7174
);
75+
76+
folderController.get(
77+
"/:folderId/share_links",
78+
verifyUserAuthentication,
79+
async (req: Request, res: Response, next: NextFunction) => {
80+
try {
81+
validateFolderRequest(req.params);
82+
validateBodyFromAuthentication(req.body);
83+
const shareLinks = await getFolderShareLinks(
84+
req.body.emailFromAuthToken,
85+
req.params.folderId
86+
);
87+
res.status(200).send({ items: shareLinks });
88+
} catch (err) {
89+
if (isValidationError(err)) {
90+
res.status(400).json({ error: err.message });
91+
return;
92+
}
93+
next(err);
94+
}
95+
}
96+
);
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
@@ -20,6 +20,8 @@ import {
2020
import { requestFieldsToDatabaseFields } from "./helper";
2121
import { getFolderAccessRole, accessRoleLessThan } from "../access/permission";
2222
import { AccessRole } from "../access/models";
23+
import { shareLinkService } from "../share_link/service";
24+
import type { ShareLink } from "../share_link/models";
2325

2426
const prettifyFolderSortType = (
2527
sortType: FolderSortOrder
@@ -178,3 +180,28 @@ export const patchFolder = async (
178180
}
179181
return result.rows[0].folderId;
180182
};
183+
184+
export const getFolderShareLinks = async (
185+
email: string,
186+
folderId: string
187+
): Promise<ShareLink[]> => {
188+
const folderShareLinkIds = await db
189+
.sql<{ id: string }>("folder.queries.get_folder_share_links", {
190+
email,
191+
folderId,
192+
})
193+
.catch((err) => {
194+
logger.error(err);
195+
throw new createError.InternalServerError(
196+
"Failed to get folder share links"
197+
);
198+
});
199+
200+
const shareLinkIds = folderShareLinkIds.rows.map((row) => row.id);
201+
const shareLinks = await shareLinkService.getShareLinks(
202+
email,
203+
[],
204+
shareLinkIds
205+
);
206+
return shareLinks;
207+
};

0 commit comments

Comments
 (0)