feat(gateway): WorkspaceService + ProjectBootstrapService + TeamsService (P8-015)
- WorkspaceService: path resolution, git init/clone, directory lifecycle (create/delete/exists), user and team root provisioning - ProjectBootstrapService: orchestrates DB record creation (via Brain) + workspace directory init in a single call - TeamsService: isMember, canAccessProject, findAll, findById, listMembers via Drizzle DB queries - WorkspaceController: POST /api/workspaces — auth-guarded project bootstrap endpoint - TeamsController: GET /api/teams, /:teamId, /:teamId/members, /:teamId/members/:userId - WorkspaceModule wired into AppModule - workspace.service.spec.ts: 5 unit tests for resolvePath (user, team, fallback, env var, default) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
73
apps/gateway/src/workspace/teams.service.ts
Normal file
73
apps/gateway/src/workspace/teams.service.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { eq, and, type Db, teams, teamMembers, projects } from '@mosaic/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
|
||||
@Injectable()
|
||||
export class TeamsService {
|
||||
private readonly logger = new Logger(TeamsService.name);
|
||||
|
||||
constructor(@Inject(DB) private readonly db: Db) {}
|
||||
|
||||
/**
|
||||
* Check if a user is a member of a team.
|
||||
*/
|
||||
async isMember(teamId: string, userId: string): Promise<boolean> {
|
||||
const rows = await this.db
|
||||
.select({ id: teamMembers.id })
|
||||
.from(teamMembers)
|
||||
.where(and(eq(teamMembers.teamId, teamId), eq(teamMembers.userId, userId)));
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check project access for a user.
|
||||
* - ownerType === 'user': project.ownerId must equal userId
|
||||
* - ownerType === 'team': userId must be a member of project.teamId
|
||||
*/
|
||||
async canAccessProject(userId: string, projectId: string): Promise<boolean> {
|
||||
const rows = await this.db
|
||||
.select({
|
||||
id: projects.id,
|
||||
ownerType: projects.ownerType,
|
||||
ownerId: projects.ownerId,
|
||||
teamId: projects.teamId,
|
||||
})
|
||||
.from(projects)
|
||||
.where(eq(projects.id, projectId));
|
||||
|
||||
const project = rows[0];
|
||||
if (!project) return false;
|
||||
|
||||
if (project.ownerType === 'user') {
|
||||
return project.ownerId === userId;
|
||||
}
|
||||
|
||||
if (project.ownerType === 'team' && project.teamId) {
|
||||
return this.isMember(project.teamId, userId);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all teams (for admin/listing endpoints).
|
||||
*/
|
||||
async findAll() {
|
||||
return this.db.select().from(teams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a team by ID.
|
||||
*/
|
||||
async findById(id: string) {
|
||||
const rows = await this.db.select().from(teams).where(eq(teams.id, id));
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* List members of a team.
|
||||
*/
|
||||
async listMembers(teamId: string) {
|
||||
return this.db.select().from(teamMembers).where(eq(teamMembers.teamId, teamId));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user