Skip to content

Commit f8afa4a

Browse files
committed
Users and Authorization
1 parent baa5125 commit f8afa4a

7 files changed

Lines changed: 116 additions & 44 deletions

File tree

packages/cli/src/cli/dev/dev-server/database.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import Datastore from "@seald-io/nedb";
2-
import type { Entity } from "@/core/resources/entity/schema.js";
32

43
export class Database {
54
private collections: Map<string, Datastore> = new Map();
65

7-
load(entities: Entity[]) {
8-
for (const entity of entities) {
9-
this.collections.set(entity.name, new Datastore());
6+
initCollections(names: string[]) {
7+
for (const name of names) {
8+
this.collections.set(name, new Datastore());
109
}
1110
}
1211

12+
hasCollection(name: string): boolean {
13+
return this.collections.has(name);
14+
}
15+
1316
getCollection(name: string): Datastore | undefined {
1417
return this.collections.get(name);
1518
}

packages/cli/src/cli/dev/dev-server/main.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ import {
1616
broadcastEntityEvent,
1717
createRealtimeServer,
1818
} from "./realtime.js";
19-
import { createEntityRoutes } from "./routes/entities.js";
19+
import { createEntityRoutes } from "./routes/entities/entities-router.js";
2020
import {
2121
createCustomIntegrationRoutes,
2222
createFileToken,
2323
createIntegrationRoutes,
2424
} from "./routes/integrations.js";
25+
import { createUsersRoutes } from "./routes/users.js";
2526
import { WatchBase44 } from "./watcher.js";
2627

2728
const DEFAULT_PORT = 4400;
@@ -76,8 +77,20 @@ export async function createDevServer(
7677
next();
7778
});
7879

80+
app.use((req, res, next) => {
81+
const auth = req.headers.authorization;
82+
if (!auth || !auth.startsWith("Bearer ")) {
83+
res.status(401).json({ error: "Unauthorized" });
84+
return;
85+
}
86+
next();
87+
});
88+
7989
const devLogger = createDevLogger();
8090

91+
const usersRoutes = createUsersRoutes();
92+
app.use("/api/apps/:appId/users", usersRoutes);
93+
8194
const functionManager = new FunctionManager(
8295
functions,
8396
devLogger,
@@ -93,19 +106,16 @@ export async function createDevServer(
93106
}
94107

95108
const db = new Database();
96-
db.load(entities);
109+
db.initCollections(entities.map((entity) => entity.name));
97110
if (db.getCollectionNames().length > 0) {
98111
clackLog.info(`Loaded entities: ${db.getCollectionNames().join(", ")}`);
99112
}
100113

101114
// Socket.IO is attached after the HTTP server starts; entity routes receive
102115
// a broadcast callback that becomes a no-op until the server is ready.
103116
let emitEntityEvent: BroadcastEntityEvent = () => {};
104-
const entityRoutes = createEntityRoutes(
105-
db,
106-
devLogger,
107-
remoteProxy,
108-
(...args) => emitEntityEvent(...args),
117+
const entityRoutes = await createEntityRoutes(db, devLogger, (...args) =>
118+
emitEntityEvent(...args),
109119
);
110120
app.use("/api/apps/:appId/entities", entityRoutes);
111121

@@ -205,7 +215,7 @@ export async function createDevServer(
205215
if (previousEntityCount > 0) {
206216
devLogger.log("Entities directory changed, clearing data...");
207217
}
208-
db.load(entities);
218+
db.initCollections(entities.map((entity) => entity.name));
209219
if (db.getCollectionNames().length > 0) {
210220
devLogger.log(
211221
`Loaded entities: ${db.getCollectionNames().join(", ")}`,

packages/cli/src/cli/dev/dev-server/routes/entities.ts renamed to packages/cli/src/cli/dev/dev-server/routes/entities/entities-router.ts

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type Datastore from "@seald-io/nedb";
2-
import type { Request, RequestHandler, Response, Router } from "express";
2+
import type { Request, Response, Router } from "express";
33
import { Router as createRouter, json } from "express";
44
import { nanoid } from "nanoid";
5-
import type { Logger } from "../../createDevLogger.js";
6-
import type { Database } from "../database.js";
7-
import type { BroadcastEntityEvent, EntityEventType } from "../realtime.js";
5+
import type { Logger } from "../../../createDevLogger.js";
6+
import type { Database } from "../../database.js";
7+
import type { BroadcastEntityEvent, EntityEventType } from "../../realtime.js";
8+
import { createUserRouter } from "./entities-user-router.js";
9+
import { stripInternalFields } from "./utils.js";
810

911
interface EntityParams {
1012
appId: string;
@@ -51,28 +53,11 @@ function parseFields(
5153
return Object.keys(projection).length > 0 ? projection : undefined;
5254
}
5355

54-
function stripInternalFields<T extends Record<string, unknown>>(
55-
doc: T[],
56-
): Omit<T, "_id">[];
57-
function stripInternalFields<T extends Record<string, unknown>>(
58-
doc: T,
59-
): Omit<T, "_id">;
60-
function stripInternalFields<T extends Record<string, unknown>>(
61-
doc: T | T[],
62-
): Omit<T, "_id"> | Omit<T, "_id">[] {
63-
if (Array.isArray(doc)) {
64-
return doc.map((d) => stripInternalFields(d));
65-
}
66-
const { _id, ...rest } = doc;
67-
return rest;
68-
}
69-
70-
export function createEntityRoutes(
56+
export async function createEntityRoutes(
7157
db: Database,
7258
logger: Logger,
73-
remoteProxy: RequestHandler,
7459
broadcast: BroadcastEntityEvent,
75-
): Router {
60+
): Promise<Router> {
7661
const router = createRouter({ mergeParams: true });
7762
const parseBody = json();
7863

@@ -116,15 +101,8 @@ export function createEntityRoutes(
116101
broadcast(appId, entityName, createData(data));
117102
}
118103

119-
router.get("/User/:id", (req, res, next) => {
120-
logger.warn(
121-
`"${req.originalUrl}" is not supported in local development, passing call to production`,
122-
);
123-
// This is necessary because Express strips the router prefix from req.url,
124-
// so without this the proxy would send just `/User/:id` instead of the full path.
125-
req.url = req.originalUrl;
126-
remoteProxy(req, res, next);
127-
});
104+
const userRouter = await createUserRouter(db);
105+
router.use("/User", userRouter);
128106

129107
router.get(
130108
"/:entityName/:id",
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Request, Response, Router } from "express";
2+
import { Router as createRouter } from "express";
3+
import { nanoid } from "nanoid";
4+
import { readAuth } from "@/core/index.js";
5+
import type { Database } from "../../database.js";
6+
import { stripInternalFields } from "./utils.js";
7+
8+
export async function createUserRouter(db: Database): Promise<Router> {
9+
if (!db.hasCollection("User")) {
10+
db.initCollections(["User"]);
11+
}
12+
13+
const userInfo = await readAuth();
14+
15+
const meEntity = await db
16+
.getCollection("User")
17+
?.findOneAsync({ email: userInfo.email });
18+
let idMe: string | undefined = meEntity?.id;
19+
20+
if (!idMe) {
21+
const now = new Date().toISOString().replace("Z", "000");
22+
idMe = nanoid();
23+
await db.getCollection("User")?.insertAsync({
24+
id: idMe,
25+
email: userInfo.email,
26+
full_name: userInfo.name,
27+
is_service: false,
28+
is_verified: true,
29+
disabled: null,
30+
role: "admin",
31+
collaborator_role: "editor",
32+
created_date: now,
33+
updated_date: now,
34+
});
35+
}
36+
37+
const router = createRouter({ mergeParams: true });
38+
39+
router.get("/:id", async (req: Request<{ id: string }>, res: Response) => {
40+
const id = req.params.id === "me" ? idMe : req.params.id;
41+
const result = await db.getCollection("User")?.findOneAsync({ id });
42+
if (!result) {
43+
res
44+
.status(404)
45+
.json({ error: `User with id "${req.params.id}" not found` });
46+
return;
47+
}
48+
res.json(stripInternalFields(result));
49+
});
50+
51+
return router;
52+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export function stripInternalFields<T extends Record<string, unknown>>(
2+
doc: T[],
3+
): Omit<T, "_id">[];
4+
export function stripInternalFields<T extends Record<string, unknown>>(
5+
doc: T,
6+
): Omit<T, "_id">;
7+
export function stripInternalFields<T extends Record<string, unknown>>(
8+
doc: T | T[],
9+
): Omit<T, "_id"> | Omit<T, "_id">[] {
10+
if (Array.isArray(doc)) {
11+
return doc.map((d) => stripInternalFields(d));
12+
}
13+
const { _id, ...rest } = doc;
14+
return rest;
15+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { json, Router } from "express";
2+
3+
export function createUsersRoutes(): Router {
4+
const router = Router({ mergeParams: true });
5+
const parseBody = json();
6+
7+
router.post("/invite-user", parseBody, (req, _res, next) => {
8+
console.log("invite-user", req.body);
9+
next();
10+
});
11+
12+
return router;
13+
}

packages/cli/tests/cli/connectors_list_available.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ describe("connectors list-available command", () => {
7373
await t.givenLoggedInWithProject(fixture("basic"));
7474
t.api.mockAvailableIntegrationsList({
7575
integrations: [{ bad: "data" }],
76+
// biome-ignore lint/suspicious/noExplicitAny: intentionally sending wrong data
7677
} as any);
7778

7879
const result = await t.run("connectors", "list-available");

0 commit comments

Comments
 (0)