BetterAuth session responses contain only identity fields — workspace context (workspaceId, currentWorkspaceId) was never returned, causing "Workspace ID is required" on every guarded endpoint after login. Add GET /api/workspaces endpoint (AuthGuard only, no WorkspaceGuard) that returns user workspace memberships with auto-provisioning for new users. Frontend auth-context now fetches workspaces after session check and persists the default to localStorage. Race condition in auto-provisioning is guarded by re-querying inside the transaction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
98 lines
2.8 KiB
TypeScript
98 lines
2.8 KiB
TypeScript
import { Injectable, Logger } from "@nestjs/common";
|
|
import { WorkspaceMemberRole } from "@prisma/client";
|
|
import { PrismaService } from "../prisma/prisma.service";
|
|
import type { WorkspaceResponseDto } from "./dto";
|
|
|
|
@Injectable()
|
|
export class WorkspacesService {
|
|
private readonly logger = new Logger(WorkspacesService.name);
|
|
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
/**
|
|
* Get all workspaces the user is a member of.
|
|
*
|
|
* Auto-provisioning: if the user has no workspace memberships (e.g. fresh
|
|
* signup via BetterAuth), a default workspace is created atomically and
|
|
* returned. This is the only call site for workspace bootstrapping.
|
|
*/
|
|
async getUserWorkspaces(userId: string): Promise<WorkspaceResponseDto[]> {
|
|
const memberships = await this.prisma.workspaceMember.findMany({
|
|
where: { userId },
|
|
include: {
|
|
workspace: {
|
|
select: { id: true, name: true, ownerId: true, createdAt: true },
|
|
},
|
|
},
|
|
orderBy: { joinedAt: "asc" },
|
|
});
|
|
|
|
if (memberships.length > 0) {
|
|
return memberships.map((m) => ({
|
|
id: m.workspace.id,
|
|
name: m.workspace.name,
|
|
ownerId: m.workspace.ownerId,
|
|
role: m.role,
|
|
createdAt: m.workspace.createdAt,
|
|
}));
|
|
}
|
|
|
|
// Auto-provision a default workspace for new users.
|
|
// Re-query inside the transaction to guard against concurrent requests
|
|
// both seeing zero memberships and creating duplicate workspaces.
|
|
this.logger.log(`Auto-provisioning default workspace for user ${userId}`);
|
|
|
|
const workspace = await this.prisma.$transaction(async (tx) => {
|
|
const existing = await tx.workspaceMember.findFirst({
|
|
where: { userId },
|
|
include: {
|
|
workspace: {
|
|
select: { id: true, name: true, ownerId: true, createdAt: true },
|
|
},
|
|
},
|
|
});
|
|
if (existing) {
|
|
return { ...existing.workspace, alreadyExisted: true as const };
|
|
}
|
|
|
|
const created = await tx.workspace.create({
|
|
data: {
|
|
name: "My Workspace",
|
|
ownerId: userId,
|
|
settings: {},
|
|
},
|
|
});
|
|
await tx.workspaceMember.create({
|
|
data: {
|
|
workspaceId: created.id,
|
|
userId,
|
|
role: WorkspaceMemberRole.OWNER,
|
|
},
|
|
});
|
|
return { ...created, alreadyExisted: false as const };
|
|
});
|
|
|
|
if (workspace.alreadyExisted) {
|
|
return [
|
|
{
|
|
id: workspace.id,
|
|
name: workspace.name,
|
|
ownerId: workspace.ownerId,
|
|
role: WorkspaceMemberRole.OWNER,
|
|
createdAt: workspace.createdAt,
|
|
},
|
|
];
|
|
}
|
|
|
|
return [
|
|
{
|
|
id: workspace.id,
|
|
name: workspace.name,
|
|
ownerId: workspace.ownerId,
|
|
role: WorkspaceMemberRole.OWNER,
|
|
createdAt: workspace.createdAt,
|
|
},
|
|
];
|
|
}
|
|
}
|