Skip to content

Commit b5a6b4d

Browse files
authored
projects: add projects and projectId scoping to assets/api-token cotrollers (#2102)
* Revert "Revert "Projects in Studio (#2078)" (#2096)" This reverts commit df62e49. * asset/api-token: update sql logic to correctly select projectId The previous queries were selecting records where projectId field in the JSONB data column was missing.
1 parent 55d9116 commit b5a6b4d

File tree

11 files changed

+388
-9
lines changed

11 files changed

+388
-9
lines changed

packages/api/src/controllers/api-token.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const fieldsMap = {
4848
name: { val: `api_token.data->>'name'`, type: "full-text" },
4949
lastSeen: `api_token.data->'lastSeen'`,
5050
userId: `api_token.data->>'userId'`,
51+
projectId: `api_token.data->>'projectId'`,
5152
"user.email": { val: `users.data->>'email'`, type: "full-text" },
5253
};
5354

@@ -66,6 +67,9 @@ app.get("/", async (req, res) => {
6667

6768
if (!userId) {
6869
const query = parseFilters(fieldsMap, filters);
70+
query.push(
71+
sql`coalesce(api_token.data->>'projectId', '') = ${req.project?.id || ""}`
72+
);
6973

7074
let fields =
7175
" api_token.id as id, api_token.data as data, users.id as usersId, users.data as usersdata";
@@ -102,9 +106,11 @@ app.get("/", async (req, res) => {
102106
errors: ["user can only request information on their own tokens"],
103107
});
104108
}
105-
106109
const query = parseFilters(fieldsMap, filters);
107110
query.push(sql`api_token.data->>'userId' = ${userId}`);
111+
query.push(
112+
sql`coalesce(api_token.data->>'projectId', '') = ${req.project?.id || ""}`
113+
);
108114

109115
let fields = " api_token.id as id, api_token.data as data";
110116
if (count) {
@@ -155,6 +161,7 @@ app.post("/", validatePost("api-token"), async (req, res) => {
155161
await req.store.create({
156162
id: id,
157163
userId: userId,
164+
projectId: req.query.projectId?.toString(),
158165
kind: "api-token",
159166
name: req.body.name,
160167
access: req.body.access,

packages/api/src/controllers/asset.test.ts

+139-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import {
66
createMockFile,
77
} from "../test-helpers";
88
import { v4 as uuid } from "uuid";
9-
import { Asset, AssetPatchPayload, Task, User } from "../schema/types";
9+
import {
10+
ApiToken,
11+
Asset,
12+
AssetPatchPayload,
13+
Task,
14+
User,
15+
} from "../schema/types";
1016
import { db } from "../store";
1117
import { WithID } from "../store/types";
1218
import Table from "../store/table";
@@ -76,6 +82,39 @@ describe("controllers/asset", () => {
7682
let nonAdminUser: User;
7783
let nonAdminToken: string;
7884

85+
const createProject = async () => {
86+
let res = await client.post(`/project`);
87+
expect(res.status).toBe(201);
88+
const project = await res.json();
89+
expect(project).toBeDefined();
90+
return project;
91+
};
92+
93+
const allowedOrigins = [
94+
"http://localhost:3000",
95+
"https://staging.wetube.com",
96+
"http://blockflix.io:69",
97+
];
98+
99+
const createApiToken = async (
100+
cors: ApiToken["access"]["cors"],
101+
projectId: string
102+
) => {
103+
client.jwtAuth = nonAdminToken;
104+
let res = await client.post(`/api-token/?projectId=${projectId}`, {
105+
name: "test",
106+
access: { cors },
107+
});
108+
client.jwtAuth = null;
109+
expect(res.status).toBe(201);
110+
const apiKeyObj = await res.json();
111+
expect(apiKeyObj).toMatchObject({
112+
id: expect.any(String),
113+
access: { cors },
114+
});
115+
return apiKeyObj.id;
116+
};
117+
79118
beforeEach(async () => {
80119
await db.objectStore.create({
81120
id: "mock_vod_store",
@@ -103,6 +142,7 @@ describe("controllers/asset", () => {
103142
type: "url",
104143
url: spec.url,
105144
},
145+
projectId: "", //should be blank when using jwt and projectId not specified as query-param
106146
status: { phase: "waiting" },
107147
});
108148

@@ -123,6 +163,7 @@ describe("controllers/asset", () => {
123163

124164
client.jwtAuth = null;
125165
client.apiKey = adminApiKey;
166+
126167
res = await client.post(`/task/${taskId}/status`, {
127168
status: {
128169
phase: "running",
@@ -148,6 +189,103 @@ describe("controllers/asset", () => {
148189
});
149190
});
150191

192+
it.only("should import asset (using jwt) for existing project (created with jwt)", async () => {
193+
const spec = {
194+
name: "test",
195+
url: "https://example.com/test.mp4",
196+
};
197+
const projectId = await createProject();
198+
199+
let res = await client.post(
200+
`/asset/upload/url/?projectId=${projectId}`,
201+
spec
202+
);
203+
expect(res.status).toBe(201);
204+
const { asset, task } = await res.json();
205+
expect(asset).toMatchObject({
206+
id: expect.any(String),
207+
name: "test",
208+
source: {
209+
type: "url",
210+
url: spec.url,
211+
},
212+
projectId: `${projectId}`,
213+
status: { phase: "waiting" },
214+
});
215+
216+
client.jwtAuth = null;
217+
client.apiKey = adminApiKey;
218+
219+
res = await client.get(`/project/${projectId}`);
220+
const project = await res.json();
221+
expect(res.status).toBe(200);
222+
expect(project).toBeDefined(); //api-key be retrieve if adminApiKey is used..
223+
});
224+
225+
it.only("should import asset (using api-token) for existing project (created with jwt)", async () => {
226+
const spec = {
227+
name: "test",
228+
url: "https://example.com/test.mp4",
229+
};
230+
const projectId = await createProject();
231+
232+
client.jwtAuth = null;
233+
client.apiKey = await createApiToken({ allowedOrigins }, projectId);
234+
235+
let res = await client.post(`/asset/upload/url/`, spec);
236+
expect(res.status).toBe(201);
237+
const { asset, task } = await res.json();
238+
expect(asset).toMatchObject({
239+
id: expect.any(String),
240+
name: "test",
241+
source: {
242+
type: "url",
243+
url: spec.url,
244+
},
245+
projectId: `${projectId}`,
246+
status: { phase: "waiting" },
247+
});
248+
249+
client.apiKey = adminApiKey;
250+
res = await client.get(`/project/${projectId}`);
251+
const project = await res.json();
252+
expect(res.status).toBe(200);
253+
expect(project.id).toBeDefined();
254+
});
255+
256+
it("should NOT import asset (using api-key) when projectId passed as ouery-param", async () => {
257+
const spec = {
258+
name: "test",
259+
url: "https://example.com/test.mp4",
260+
};
261+
262+
client.jwtAuth = null;
263+
client.apiKey = adminApiKey;
264+
265+
const projectId = await createProject();
266+
267+
// BadRequest is expected if projectId is passed in as query-param
268+
let res = await client.post(
269+
`/asset/upload/url/?projectId=${projectId}`,
270+
spec
271+
);
272+
expect(res.status).toBe(400);
273+
274+
// Let's try again without query-param
275+
res = await client.post(`/asset/upload/url/`, spec);
276+
const { asset, task } = await res.json();
277+
expect(asset).toMatchObject({
278+
id: expect.any(String),
279+
name: "test",
280+
source: {
281+
type: "url",
282+
url: spec.url,
283+
},
284+
projectId: "", //should be blank when using an existing api-key and new project was created
285+
status: { phase: "waiting" },
286+
});
287+
});
288+
151289
it("should detect duplicate assets", async () => {
152290
const spec = {
153291
name: "test",

packages/api/src/controllers/asset.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
NewAssetPayload,
4646
ObjectStore,
4747
PlaybackPolicy,
48+
Project,
4849
Task,
4950
} from "../schema/types";
5051
import { WithID } from "../store/types";
@@ -183,7 +184,7 @@ function parseUrlToDStorageUrl(
183184
}
184185

185186
export async function validateAssetPayload(
186-
req: Pick<Request, "body" | "user" | "token" | "config">,
187+
req: Pick<Request, "body" | "user" | "token" | "config" | "project">,
187188
id: string,
188189
playbackId: string,
189190
createdAt: number,
@@ -235,6 +236,7 @@ export async function validateAssetPayload(
235236
name: payload.name,
236237
source,
237238
staticMp4: payload.staticMp4,
239+
projectId: req.project?.id ?? "",
238240
creatorId: mapInputCreatorId(payload.creatorId),
239241
playbackPolicy,
240242
objectStoreId: payload.objectStoreId || (await defaultObjectStoreId(req)),
@@ -614,6 +616,7 @@ const fieldsMap = {
614616
creatorId: `asset.data->'creatorId'->>'value'`,
615617
playbackId: `asset.data->>'playbackId'`,
616618
playbackRecordingId: `asset.data->>'playbackRecordingId'`,
619+
projectId: `asset.data->>'projectId'`,
617620
phase: `asset.data->'status'->>'phase'`,
618621
"user.email": { val: `users.data->>'email'`, type: "full-text" },
619622
cid: `asset.data->'storage'->'ipfs'->>'cid'`,
@@ -655,6 +658,10 @@ app.get("/", authorizer({}), async (req, res) => {
655658
query.push(sql`asset.data->>'deleted' IS NULL`);
656659
}
657660

661+
query.push(
662+
sql`coalesce(asset.data->>'projectId', '') = ${req.project?.id || ""}`
663+
);
664+
658665
let output: WithID<Asset>[];
659666
let newCursor: string;
660667
if (req.user.admin && allUsers && allUsers !== "false") {
@@ -799,7 +806,11 @@ const uploadWithUrlHandler: RequestHandler = async (req, res) => {
799806
url,
800807
encryption: assetEncryptionWithoutKey(encryption),
801808
});
802-
const dupAsset = await db.asset.findDuplicateUrlUpload(url, req.user.id);
809+
const dupAsset = await db.asset.findDuplicateUrlUpload(
810+
url,
811+
req.user.id,
812+
req.project?.id
813+
);
803814
if (dupAsset) {
804815
const [task] = await db.task.find({ outputAssetId: dupAsset.id });
805816
if (!task.length) {

packages/api/src/controllers/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import session from "./session";
2424
import playback from "./playback";
2525
import did from "./did";
2626
import room from "./room";
27+
import project from "./project";
2728

2829
// Annoying but necessary to get the routing correct
2930
export default {
@@ -53,4 +54,5 @@ export default {
5354
did,
5455
room,
5556
clip,
57+
project,
5658
};

0 commit comments

Comments
 (0)