Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Projects integration #2057

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions packages/api/src/controllers/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
NewAssetPayload,
ObjectStore,
PlaybackPolicy,
Project,
Task,
} from "../schema/types";
import { WithID } from "../store/types";
Expand All @@ -57,6 +58,7 @@ import {
import { CliArgs } from "../parse-cli";
import mung from "express-mung";
import { getClips } from "./clip";
import { getProject } from "./project";

const app = Router();

Expand Down Expand Up @@ -200,6 +202,13 @@ export async function validateAssetPayload(
}
}

if (payload.projectId) {
const project = await getProject(req);
if (project.userId != userId) {
throw new ForbiddenError(`the provided projectId is not owned by user`);
}
}

// Validate playbackPolicy on creation to generate resourceId & check if unifiedAccessControlConditions is present when using lit_signing_condition
const playbackPolicy = await validateAssetPlaybackPolicy(
payload,
Expand Down Expand Up @@ -234,6 +243,7 @@ export async function validateAssetPayload(
name: payload.name,
source,
staticMp4: payload.staticMp4,
projectId: payload.projectId,
creatorId: mapInputCreatorId(payload.creatorId),
playbackPolicy,
objectStoreId: payload.objectStoreId || (await defaultObjectStoreId(req)),
Expand Down Expand Up @@ -625,8 +635,18 @@ const fieldsMap = {
} as const;

app.get("/", authorizer({}), async (req, res) => {
let { limit, cursor, all, allUsers, order, filters, count, cid, ...otherQs } =
toStringValues(req.query);
let {
limit,
cursor,
all,
allUsers,
order,
filters,
count,
cid,
projectId,
...otherQs
} = toStringValues(req.query);
const fieldFilters = _(otherQs)
.pick("playbackId", "sourceUrl", "phase")
.map((v, k) => ({ id: k, value: decodeURIComponent(v) }))
Expand Down Expand Up @@ -654,6 +674,14 @@ app.get("/", authorizer({}), async (req, res) => {
query.push(sql`asset.data->>'deleted' IS NULL`);
}

if (projectId) {
query.push(sql`asset.data->>'projectId' = ${projectId}`);
} else {
query.push(
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 @@ -774,7 +802,7 @@ app.post(
);

const uploadWithUrlHandler: RequestHandler = async (req, res) => {
let { url, encryption, c2pa, profiles, targetSegmentSizeSecs } =
let { url, encryption, c2pa, profiles, targetSegmentSizeSecs, projectId } =
req.body as NewAssetPayload;
if (!url) {
return res.status(422).json({
Expand All @@ -798,7 +826,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,
projectId
);
if (dupAsset) {
const [task] = await db.task.find({ outputAssetId: dupAsset.id });
if (!task.length) {
Expand Down
4 changes: 4 additions & 0 deletions packages/api/src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import session from "./session";
import playback from "./playback";
import did from "./did";
import room from "./room";
import project from "./project";
import workspace from "./workspace";

// Annoying but necessary to get the routing correct
export default {
Expand Down Expand Up @@ -53,4 +55,6 @@ export default {
did,
room,
clip,
project,
workspace,
};
161 changes: 161 additions & 0 deletions packages/api/src/controllers/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { Request, RequestHandler, Router } from "express";
import { authorizer, validatePost } from "../middleware";
import { db } from "../store";
import { v4 as uuid } from "uuid";
import {
makeNextHREF,
parseFilters,
parseOrder,
getS3PresignedUrl,
toStringValues,
pathJoin,
getObjectStoreS3Config,
reqUseReplica,
isValidBase64,
mapInputCreatorId,
} from "./helpers";
import sql from "sql-template-strings";
import {
ForbiddenError,
UnprocessableEntityError,
NotFoundError,
BadRequestError,
InternalServerError,
UnauthorizedError,
NotImplementedError,
} from "../store/errors";

const app = Router();

export async function getProject(req) {
const projectId = req.params.projectId || req.body.projectId;
console.log("XXX: getting project:", projectId);

const project = await db.project.get(projectId);
if (!project || project.deleted) {
throw new NotFoundError(`project not found`);
}

if (!req.user.admin && req.user.id !== project.userId) {
throw new ForbiddenError(`invalid user`);
}

return project;
}

const fieldsMap = {
id: `project.ID`,
name: { val: `project.data->>'name'`, type: "full-text" },
createdAt: { val: `project.data->'createdAt'`, type: "int" },
userId: `project.data->>'userId'`,
} as const;

app.get("/", authorizer({}), async (req, res) => {
Fixed Show fixed Hide fixed
Dismissed Show dismissed Hide dismissed
let { limit, cursor, order, all, filters, count } = toStringValues(req.query);

if (isNaN(parseInt(limit))) {
limit = undefined;
}
if (!order) {
order = "updatedAt-true,createdAt-true";
}

const query = [...parseFilters(fieldsMap, filters)];

if (!req.user.admin || !all || all === "false") {
query.push(sql`project.data->>'deleted' IS NULL`);
}

let output: WithID<Project>[];
let newCursor: string;
if (req.user.admin) {
let fields =
" project.id as id, project.data as data, users.id as usersId, users.data as usersdata";
if (count) {
fields = fields + ", count(*) OVER() AS count";
}
const from = `project left join users on project.data->>'userId' = users.id`;
[output, newCursor] = await db.project.find(query, {
limit,
cursor,
fields,
from,
order: parseOrder(fieldsMap, order),
process: ({ data, usersdata, count: c }) => {
if (count) {
res.set("X-Total-Count", c);
}
return {
...data,
user: db.user.cleanWriteOnlyResponse(usersdata),
};
},
});
} else {
query.push(sql`project.data->>'userId' = ${req.user.id}`);

let fields = " project.id as id, project.data as data";
if (count) {
fields = fields + ", count(*) OVER() AS count";
}
[output, newCursor] = await db.project.find(query, {
limit,
cursor,
fields,
order: parseOrder(fieldsMap, order),
process: ({ data, count: c }) => {
if (count) {
res.set("X-Total-Count", c);
}
return data;
},
});
}

res.status(200);
if (output.length > 0 && newCursor) {
res.links({ next: makeNextHREF(req, newCursor) });
}

return res.json(output);
});

app.get("/:projectId", authorizer({}), async (req, res) => {
Dismissed Show dismissed Hide dismissed
const project = await getProject(req);

if (!project) {
res.status(403);
return res.json({ errors: ["project not found"] });
}

res.status(200);
res.json(project);
});

app.post("/", authorizer({}), async (req, res) => {
Fixed Show fixed Hide fixed

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
const { name } = req.body;

console.log("XXX: req.user", req.user);
console.log("XXX: req.query", req.query);

const id = uuid();
await db.project.create({
id: id,
name: name,
userId: req.user.id,
createdAt: Date.now(),
});
res.status(201);

const project = await db.project.get(id, { useReplica: false });

if (!project) {
res.status(403);
return res.json({ errors: ["project not created"] });
}

res.status(201);
res.json(id);
});

export default app;
12 changes: 12 additions & 0 deletions packages/api/src/controllers/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,8 @@ app.get("/", authorizer({}), async (req, res) => {
filters,
userId,
count,
projectId,
workspaceId,
} = toStringValues(req.query);
if (isNaN(parseInt(limit))) {
limit = undefined;
Expand All @@ -396,6 +398,8 @@ app.get("/", authorizer({}), async (req, res) => {
userId = req.user.id;
}

console.log(`DEBUG: req.query: ${JSON.stringify(req.query)}`);

const query = parseFilters(fieldsMap, filters);
if (!all || all === "false" || !req.user.admin) {
query.push(sql`stream.data->>'deleted' IS NULL`);
Expand All @@ -416,6 +420,13 @@ app.get("/", authorizer({}), async (req, res) => {
if (userId) {
query.push(sql`stream.data->>'userId' = ${userId}`);
}
if (projectId) {
query.push(sql`stream.data->>'projectId' = ${projectId}`);
} else {
query.push(sql`stream.data->>'projectId' IS NULL`);
}
// workspaceId will initially be all NULL (which is default)
query.push(sql`stream.data->>'workspaceId' IS NULL`);

if (!order) {
order = "lastSeen-true,createdAt-true";
Expand Down Expand Up @@ -1940,6 +1951,7 @@ app.post(
app.delete("/:id/terminate", authorizer({}), async (req, res) => {
const { id } = req.params;
const stream = await db.stream.get(id);

if (
!stream ||
(!req.user.admin && (stream.deleted || stream.userId !== req.user.id))
Expand Down
26 changes: 26 additions & 0 deletions packages/api/src/controllers/workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Request, RequestHandler, Router } from "express";
import { authorizer, validatePost } from "../middleware";
import { db } from "../store";
import { v4 as uuid } from "uuid";

const app = Router();

app.get("/", authorizer({}), async (req, res) => {

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
res.status(200);
res.json({});
});

app.post("/", authorizer({}), async (req, res) => {

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
const { name } = req.body;

const id = uuid();
await db.workspace.create({
id: id,
name: "foo",
userId: req.user.id,
createdAt: Date.now(),
});
res.status(201).end();
});

export default app;
42 changes: 42 additions & 0 deletions packages/api/src/schema/api-schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,44 @@ components:
type: string
url:
$ref: "#/components/schemas/multistream-target/properties/url"
project:
type: object
required:
- name
additionalProperties: false
properties:
id:
type: string
readOnly: true
example: de7818e7-610a-4057-8f6f-b785dc1e6f88
name:
type: string
example: test_project
createdAt:
type: number
readOnly: true
description:
Timestamp (in milliseconds) at which stream object was created
example: 1587667174725
workspace:
type: object
required:
- name
additionalProperties: false
properties:
id:
type: string
readOnly: true
example: de7818e7-610a-4057-8f6f-b785dc1e6f88
name:
type: string
example: test_workspace
createdAt:
type: number
readOnly: true
description:
Timestamp (in milliseconds) at which stream object was created
example: 1587667174725
stream:
type: object
required:
Expand Down Expand Up @@ -1153,6 +1191,10 @@ components:
Name of the asset. This is not necessarily the filename, can be a
custom name or title
example: filename.mp4
projectId:
type: string
description: The ID of the project
example: aac12556-4d65-4d34-9fb6-d1f0985eb0a9
staticMp4:
type: boolean
description: Whether to generate MP4s for the asset.
Expand Down
Loading
Loading