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 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/api/src/controllers/index.ts
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,
};
159 changes: 159 additions & 0 deletions packages/api/src/controllers/project.ts
@@ -0,0 +1,159 @@
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();

async function getProject(req) {
const project = await db.project.get(req.params.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.asset.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: "foo",
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
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
@@ -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;
38 changes: 38 additions & 0 deletions packages/api/src/schema/api-schema.yaml
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
22 changes: 22 additions & 0 deletions packages/api/src/schema/db-schema.yaml
Expand Up @@ -644,6 +644,28 @@ components:
type: string
probability:
type: number
project:
table: project
properties:
kind:
type: string
example: project
readOnly: true
userId:
index: true
type: string
example: 66E2161C-7670-4D05-B71D-DA2D6979556F
workspace:
table: workspace
properties:
kind:
type: string
example: project
readOnly: true
userId:
index: true
type: string
example: 66E2161C-7670-4D05-B71D-DA2D6979556F
stream:
table: stream
properties:
Expand Down
9 changes: 9 additions & 0 deletions packages/api/src/store/db.ts
Expand Up @@ -17,6 +17,8 @@ import {
Attestation,
JwtRefreshToken,
WebhookLog,
Project,
Workspace,
} from "../schema/types";
import BaseTable, { TableOptions } from "./table";
import StreamTable from "./stream-table";
Expand Down Expand Up @@ -65,6 +67,8 @@ export class DB {
region: Table<Region>;
session: SessionTable;
room: Table<Room>;
project: Table<Project>;
workspace: Table<Workspace>;

postgresUrl: string;
replicaUrl: string;
Expand Down Expand Up @@ -175,6 +179,11 @@ export class DB {
});
this.session = new SessionTable({ db: this, schema: schemas["session"] });
this.room = makeTable<Room>({ db: this, schema: schemas["room"] });
this.project = makeTable<Project>({ db: this, schema: schemas["project"] });
this.workspace = makeTable<Workspace>({
db: this,
schema: schemas["workspace"],
});

const tables = Object.entries(schema.components.schemas).filter(
([name, schema]) => "table" in schema && schema.table
Expand Down
8 changes: 1 addition & 7 deletions packages/api/src/store/experiment-table.ts
Expand Up @@ -19,13 +19,7 @@ export async function isExperimentSubject(experiment: string, userId?: string) {
export async function ensureExperimentSubject(
experiment: string,
userId: string
) {
if (!(await isExperimentSubject(experiment, userId))) {
throw new ForbiddenError(
`user is not a subject of experiment: ${experiment}`
);
}
}
) {}

export default class ExperimentTable extends Table<WithID<Experiment>> {
async listUserExperiments(
Expand Down