Skip to content

Commit

Permalink
Projects in Studio (#2078)
Browse files Browse the repository at this point in the history
* schema: add boiler plate to handle projectId logic

* project: add handlers to get/post projects

* auth: add projectId support for api-token and jwt methods

* api-token: add projectId support in api-key handlers

* asset: add projectId support in asset handlers
  • Loading branch information
emranemran authored Mar 11, 2024
1 parent a95ee1c commit 6c20637
Show file tree
Hide file tree
Showing 11 changed files with 399 additions and 9 deletions.
11 changes: 10 additions & 1 deletion packages/api/src/controllers/api-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const fieldsMap = {
name: { val: `api_token.data->>'name'`, type: "full-text" },
lastSeen: `api_token.data->'lastSeen'`,
userId: `api_token.data->>'userId'`,
projectId: `api_token.data->>'projectId'`,
"user.email": { val: `users.data->>'email'`, type: "full-text" },
};

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

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

let fields =
" api_token.id as id, api_token.data as data, users.id as usersId, users.data as usersdata";
Expand Down Expand Up @@ -102,9 +106,13 @@ app.get("/", async (req, res) => {
errors: ["user can only request information on their own tokens"],
});
}

const query = parseFilters(fieldsMap, filters);
query.push(sql`api_token.data->>'userId' = ${userId}`);
query.push(
req.project?.id
? sql`api_token.data->>'projectId' = ${req.project.id}`
: sql`api_token.data->>'projectId' IS NULL OR api_token.data->>'projectId' = ''`
);

let fields = " api_token.id as id, api_token.data as data";
if (count) {
Expand Down Expand Up @@ -155,6 +163,7 @@ app.post("/", validatePost("api-token"), async (req, res) => {
await req.store.create({
id: id,
userId: userId,
projectId: req.query.projectId?.toString(),
kind: "api-token",
name: req.body.name,
access: req.body.access,
Expand Down
140 changes: 139 additions & 1 deletion packages/api/src/controllers/asset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import {
createMockFile,
} from "../test-helpers";
import { v4 as uuid } from "uuid";
import { Asset, AssetPatchPayload, Task, User } from "../schema/types";
import {
ApiToken,
Asset,
AssetPatchPayload,
Task,
User,
} from "../schema/types";
import { db } from "../store";
import { WithID } from "../store/types";
import Table from "../store/table";
Expand Down Expand Up @@ -76,6 +82,39 @@ describe("controllers/asset", () => {
let nonAdminUser: User;
let nonAdminToken: string;

const createProject = async () => {
let res = await client.post(`/project`);
expect(res.status).toBe(201);
const project = await res.json();
expect(project).toBeDefined();
return project;
};

const allowedOrigins = [
"http://localhost:3000",
"https://staging.wetube.com",
"http://blockflix.io:69",
];

const createApiToken = async (
cors: ApiToken["access"]["cors"],
projectId: string
) => {
client.jwtAuth = nonAdminToken;
let res = await client.post(`/api-token/?projectId=${projectId}`, {
name: "test",
access: { cors },
});
client.jwtAuth = null;
expect(res.status).toBe(201);
const apiKeyObj = await res.json();
expect(apiKeyObj).toMatchObject({
id: expect.any(String),
access: { cors },
});
return apiKeyObj.id;
};

beforeEach(async () => {
await db.objectStore.create({
id: "mock_vod_store",
Expand Down Expand Up @@ -103,6 +142,7 @@ describe("controllers/asset", () => {
type: "url",
url: spec.url,
},
projectId: "", //should be blank when using jwt and projectId not specified as query-param
status: { phase: "waiting" },
});

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

client.jwtAuth = null;
client.apiKey = adminApiKey;

res = await client.post(`/task/${taskId}/status`, {
status: {
phase: "running",
Expand All @@ -148,6 +189,103 @@ describe("controllers/asset", () => {
});
});

it.only("should import asset (using jwt) for existing project (created with jwt)", async () => {
const spec = {
name: "test",
url: "https://example.com/test.mp4",
};
const projectId = await createProject();

let res = await client.post(
`/asset/upload/url/?projectId=${projectId}`,
spec
);
expect(res.status).toBe(201);
const { asset, task } = await res.json();
expect(asset).toMatchObject({
id: expect.any(String),
name: "test",
source: {
type: "url",
url: spec.url,
},
projectId: `${projectId}`,
status: { phase: "waiting" },
});

client.jwtAuth = null;
client.apiKey = adminApiKey;

res = await client.get(`/project/${projectId}`);
const project = await res.json();
expect(res.status).toBe(200);
expect(project).toBeDefined(); //api-key be retrieve if adminApiKey is used..
});

it.only("should import asset (using api-token) for existing project (created with jwt)", async () => {
const spec = {
name: "test",
url: "https://example.com/test.mp4",
};
const projectId = await createProject();

client.jwtAuth = null;
client.apiKey = await createApiToken({ allowedOrigins }, projectId);

let res = await client.post(`/asset/upload/url/`, spec);
expect(res.status).toBe(201);
const { asset, task } = await res.json();
expect(asset).toMatchObject({
id: expect.any(String),
name: "test",
source: {
type: "url",
url: spec.url,
},
projectId: `${projectId}`,
status: { phase: "waiting" },
});

client.apiKey = adminApiKey;
res = await client.get(`/project/${projectId}`);
const project = await res.json();
expect(res.status).toBe(200);
expect(project.id).toBeDefined();
});

it("should NOT import asset (using api-key) when projectId passed as ouery-param", async () => {
const spec = {
name: "test",
url: "https://example.com/test.mp4",
};

client.jwtAuth = null;
client.apiKey = adminApiKey;

const projectId = await createProject();

// BadRequest is expected if projectId is passed in as query-param
let res = await client.post(
`/asset/upload/url/?projectId=${projectId}`,
spec
);
expect(res.status).toBe(400);

// Let's try again without query-param
res = await client.post(`/asset/upload/url/`, spec);
const { asset, task } = await res.json();
expect(asset).toMatchObject({
id: expect.any(String),
name: "test",
source: {
type: "url",
url: spec.url,
},
projectId: "", //should be blank when using an existing api-key and new project was created
status: { phase: "waiting" },
});
});

it("should detect duplicate assets", async () => {
const spec = {
name: "test",
Expand Down
17 changes: 15 additions & 2 deletions packages/api/src/controllers/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
NewAssetPayload,
ObjectStore,
PlaybackPolicy,
Project,
Task,
} from "../schema/types";
import { WithID } from "../store/types";
Expand Down Expand Up @@ -183,7 +184,7 @@ function parseUrlToDStorageUrl(
}

export async function validateAssetPayload(
req: Pick<Request, "body" | "user" | "token" | "config">,
req: Pick<Request, "body" | "user" | "token" | "config" | "project">,
id: string,
playbackId: string,
createdAt: number,
Expand Down Expand Up @@ -235,6 +236,7 @@ export async function validateAssetPayload(
name: payload.name,
source,
staticMp4: payload.staticMp4,
projectId: req.project?.id ?? "",
creatorId: mapInputCreatorId(payload.creatorId),
playbackPolicy,
objectStoreId: payload.objectStoreId || (await defaultObjectStoreId(req)),
Expand Down Expand Up @@ -614,6 +616,7 @@ const fieldsMap = {
creatorId: `asset.data->'creatorId'->>'value'`,
playbackId: `asset.data->>'playbackId'`,
playbackRecordingId: `asset.data->>'playbackRecordingId'`,
projectId: `asset.data->>'projectId'`,
phase: `asset.data->'status'->>'phase'`,
"user.email": { val: `users.data->>'email'`, type: "full-text" },
cid: `asset.data->'storage'->'ipfs'->>'cid'`,
Expand Down Expand Up @@ -655,6 +658,12 @@ app.get("/", authorizer({}), async (req, res) => {
query.push(sql`asset.data->>'deleted' IS NULL`);
}

query.push(
req.project?.id
? sql`asset.data->>'projectId' = ${req.project.id}`
: sql`asset.data->>'projectId' IS NULL OR asset.data->>'projectId' = ''`
);

let output: WithID<Asset>[];
let newCursor: string;
if (req.user.admin && allUsers && allUsers !== "false") {
Expand Down Expand Up @@ -799,7 +808,11 @@ const uploadWithUrlHandler: RequestHandler = async (req, res) => {
url,
encryption: assetEncryptionWithoutKey(encryption),
});
const dupAsset = await db.asset.findDuplicateUrlUpload(url, req.user.id);
const dupAsset = await db.asset.findDuplicateUrlUpload(
url,
req.user.id,
req.project?.id
);
if (dupAsset) {
const [task] = await db.task.find({ outputAssetId: dupAsset.id });
if (!task.length) {
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import session from "./session";
import playback from "./playback";
import did from "./did";
import room from "./room";
import project from "./project";

// Annoying but necessary to get the routing correct
export default {
Expand Down Expand Up @@ -53,4 +54,5 @@ export default {
did,
room,
clip,
project,
};
Loading

0 comments on commit 6c20637

Please sign in to comment.