feat(workspaces): implement organization-based multi-tenancy and shared workspaces

This commit is contained in:
mkorwel
2026-03-19 09:56:08 -07:00
parent c5893876bd
commit b4f1dab82e
3 changed files with 49 additions and 4 deletions
@@ -10,6 +10,7 @@ export interface AuthenticatedRequest extends Request {
user: {
id: string;
email: string;
org_id?: string;
};
}
@@ -20,6 +21,7 @@ export interface AuthenticatedRequest extends Request {
export const iapMiddleware = (req: Request, res: Response, next: NextFunction) => {
const userEmail = req.header('x-goog-authenticated-user-email');
const userId = req.header('x-goog-authenticated-user-id');
const orgId = req.header('x-goog-authenticated-user-org');
// If running locally or without IAP, use a dev user
if (!userEmail || !userId) {
@@ -28,9 +30,10 @@ export const iapMiddleware = (req: Request, res: Response, next: NextFunction) =
return;
}
(req as AuthenticatedRequest).user = {
(req as unknown as AuthenticatedRequest).user = {
id: 'dev-user-id',
email: 'dev-user@google.com',
org_id: 'dev-org-id',
};
next();
return;
@@ -40,9 +43,10 @@ export const iapMiddleware = (req: Request, res: Response, next: NextFunction) =
const cleanId = userId.replace('accounts.google.com:', '');
const cleanEmail = userEmail.replace('accounts.google.com:', '');
(req as AuthenticatedRequest).user = {
(req as unknown as AuthenticatedRequest).user = {
id: cleanId,
email: cleanEmail,
org_id: orgId,
};
next();
@@ -20,12 +20,17 @@ const CreateWorkspaceSchema = z.object({
machineType: z.string().optional().default('e2-standard-4'),
imageTag: z.string().optional().default('latest'),
zone: z.string().optional().default('us-west1-a'),
orgId: z.string().optional(),
repoId: z.string().optional(),
});
router.get('/', async (req, res) => {
try {
const authReq = req as unknown as AuthenticatedRequest;
const workspaces = await workspaceService.listWorkspaces(authReq.user.id);
const workspaces = await workspaceService.listWorkspacesForUser(
authReq.user.id,
authReq.user.org_id
);
res.json(workspaces);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
@@ -42,7 +47,7 @@ router.post('/', async (req, res) => {
return;
}
const { name, machineType, imageTag, zone } = validation.data;
const { name, machineType, imageTag, zone, orgId, repoId } = validation.data;
const workspaceId = uuidv4();
const instanceName = `workspace-${workspaceId.slice(0, 8)}`;
@@ -57,6 +62,8 @@ router.post('/', async (req, res) => {
project_id: computeService.getProjectId(),
created_at: now,
last_connected_at: now,
org_id: orgId || authReq.user.org_id,
repo_id: repoId,
};
// 1. Save to state store
@@ -16,6 +16,9 @@ export interface WorkspaceData {
project_id: string;
created_at: string;
last_connected_at: string;
team_id?: string;
org_id?: string;
repo_id?: string;
}
export interface WorkspaceRecord extends WorkspaceData {
@@ -46,6 +49,37 @@ export class WorkspaceService {
});
}
async listWorkspacesForUser(ownerId: string, orgId?: string): Promise<WorkspaceRecord[]> {
// 1. Get workspaces owned by user
const userSnapshot = await this.getCollection()
.where('owner_id', '==', ownerId)
.get();
const userWorkspaces = userSnapshot.docs.map(doc => ({
id: doc.id,
...(doc.data() as WorkspaceData),
}));
// 2. Get workspaces shared with user's org (if orgId provided)
if (orgId) {
const orgSnapshot = await this.getCollection()
.where('org_id', '==', orgId)
.get();
const orgWorkspaces = orgSnapshot.docs.map(doc => ({
id: doc.id,
...(doc.data() as WorkspaceData),
}));
// Combine and deduplicate by ID
const combined = [...userWorkspaces, ...orgWorkspaces];
const unique = Array.from(new Map(combined.map(ws => [ws.id, ws])).values());
return unique;
}
return userWorkspaces;
}
async listWorkspaces(ownerId: string): Promise<WorkspaceRecord[]> {
const snapshot = await this.getCollection()
.where('owner_id', '==', ownerId)