PRIMEIRO ENVIO

This commit is contained in:
2025-12-25 12:02:07 -03:00
parent ca49575224
commit c4d3bd9a86
92 changed files with 26976 additions and 1 deletions
+507
View File
@@ -0,0 +1,507 @@
import express from 'express';
import { AuthService } from '../services/auth.service';
import { authenticateToken, blacklistToken } from '../middleware/auth';
import config from '../config';
// Temporary mock implementations
const prisma = {
user: {
findUnique: async (params: any): Promise<any> => {
return {
id: 'mock-user-id',
email: 'mock@example.com',
firstName: 'Mock',
lastName: 'User',
subscription: 'FREE',
isActive: true,
passwordHash: '$2b$12$mockhashedpassword',
createdAt: new Date(),
usageMetrics: {
pagesCloned: 0,
storageUsed: 0,
apiCalls: 0,
lastReset: new Date()
}
};
},
create: async (params: any): Promise<any> => ({
id: 'mock-user-id',
email: params.data.email,
firstName: params.data.firstName,
lastName: params.data.lastName,
subscription: 'FREE',
createdAt: new Date()
}),
update: async (params: any): Promise<any> => ({
id: 'mock-user-id',
email: 'mock@example.com',
firstName: 'Mock',
lastName: 'User',
subscription: 'FREE',
updatedAt: new Date()
})
},
userSession: {
create: async (params: any): Promise<any> => ({ id: 'mock-session' }),
deleteMany: async (params: any): Promise<any> => ({ count: 0 })
}
};
const validateUserRegistration = (data: any) => {
if (!data.email || !data.password) {
throw new Error('Email and password are required');
}
return data;
};
const validateUserLogin = (data: any) => {
if (!data.email || !data.password) {
throw new Error('Email and password are required');
}
return data;
};
const router = express.Router();
// Register new user
router.post('/register', async (req, res, next) => {
try {
// Validate input
const validatedData = validateUserRegistration(req.body);
// Check if user already exists
const existingUser = await prisma.user.findUnique({
where: { email: validatedData.email }
});
if (existingUser) {
return res.status(400).json({
success: false,
error: 'User with this email already exists',
code: 'USER_EXISTS'
});
}
// Hash password
const passwordHash = await AuthService.hashPassword(validatedData.password);
// Create user
const user = await prisma.user.create({
data: {
email: validatedData.email,
passwordHash,
firstName: validatedData.firstName,
lastName: validatedData.lastName,
subscription: 'FREE',
usageMetrics: {
create: {
pagesCloned: 0,
storageUsed: 0,
apiCalls: 0
}
}
},
select: {
id: true,
email: true,
firstName: true,
lastName: true,
subscription: true,
createdAt: true
}
});
// Generate JWT token pair
const tokenPair = AuthService.generateTokenPair(
user.id,
user.email,
user.subscription
);
res.status(201).json({
success: true,
data: {
user,
...tokenPair
},
message: 'User registered successfully'
});
} catch (error) {
next(error);
}
});
// Login user
router.post('/login', async (req, res, next) => {
try {
// Validate input
const validatedData = validateUserLogin(req.body);
// Find user
const user = await prisma.user.findUnique({
where: { email: validatedData.email },
select: {
id: true,
email: true,
passwordHash: true,
firstName: true,
lastName: true,
subscription: true,
isActive: true
}
});
if (!user || !user.isActive) {
return res.status(401).json({
success: false,
error: 'Invalid email or password',
code: 'INVALID_CREDENTIALS'
});
}
// Verify password
const isValidPassword = await AuthService.verifyPassword(validatedData.password, user.passwordHash);
if (!isValidPassword) {
return res.status(401).json({
success: false,
error: 'Invalid email or password',
code: 'INVALID_CREDENTIALS'
});
}
// Generate JWT token pair
const tokenPair = AuthService.generateTokenPair(
user.id,
user.email,
user.subscription
);
// Create session record
await prisma.userSession.create({
data: {
userId: user.id,
token: tokenPair.accessToken,
expiresAt: new Date(Date.now() + tokenPair.expiresIn * 1000)
}
});
res.json({
success: true,
data: {
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
subscription: user.subscription
},
...tokenPair
},
message: 'Login successful'
});
} catch (error) {
next(error);
}
});
// Logout user
router.post('/logout', authenticateToken, async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];
if (token) {
// Blacklist the access token
blacklistToken(token);
// Remove session from database
await prisma.userSession.deleteMany({
where: {
userId: req.user!.id,
token
}
});
}
res.json({
success: true,
message: 'Logout successful'
});
} catch (error) {
next(error);
}
});
// Refresh token endpoint
router.post('/refresh', async (req, res, next) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({
success: false,
error: 'Refresh token is required',
code: 'VALIDATION_ERROR'
});
}
// Generate new token pair using refresh token
const newTokenPair = AuthService.refreshAccessToken(refreshToken);
res.json({
success: true,
data: newTokenPair,
message: 'Token refreshed successfully'
});
} catch (error) {
return res.status(401).json({
success: false,
error: 'Invalid or expired refresh token',
code: 'INVALID_REFRESH_TOKEN'
});
}
});
// Logout from all devices
router.post('/logout-all', authenticateToken, async (req, res, next) => {
try {
// Revoke all tokens for the user
AuthService.revokeAllUserTokens(req.user!.id);
// Remove all sessions from database
await prisma.userSession.deleteMany({
where: { userId: req.user!.id }
});
res.json({
success: true,
message: 'Logged out from all devices successfully'
});
} catch (error) {
next(error);
}
});
// Get current user profile
router.get('/me', authenticateToken, async (req, res, next) => {
try {
const user = await prisma.user.findUnique({
where: { id: req.user!.id },
select: {
id: true,
email: true,
firstName: true,
lastName: true,
avatar: true,
subscription: true,
emailVerified: true,
createdAt: true,
usageMetrics: {
select: {
pagesCloned: true,
storageUsed: true,
apiCalls: true,
lastReset: true
}
}
}
});
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found',
code: 'USER_NOT_FOUND'
});
}
res.json({
success: true,
data: user
});
} catch (error) {
next(error);
}
});
// Update user profile
router.patch('/me', authenticateToken, async (req, res, next) => {
try {
const { firstName, lastName, avatar } = req.body;
const updatedUser = await prisma.user.update({
where: { id: req.user!.id },
data: {
...(firstName && { firstName }),
...(lastName && { lastName }),
...(avatar && { avatar })
},
select: {
id: true,
email: true,
firstName: true,
lastName: true,
avatar: true,
subscription: true,
updatedAt: true
}
});
res.json({
success: true,
data: updatedUser,
message: 'Profile updated successfully'
});
} catch (error) {
next(error);
}
});
// Change password
router.post('/change-password', authenticateToken, async (req, res, next) => {
try {
const { currentPassword, newPassword } = req.body;
if (!currentPassword || !newPassword) {
return res.status(400).json({
success: false,
error: 'Current password and new password are required',
code: 'VALIDATION_ERROR'
});
}
if (newPassword.length < 8) {
return res.status(400).json({
success: false,
error: 'New password must be at least 8 characters long',
code: 'VALIDATION_ERROR'
});
}
// Get current user with password
const user = await prisma.user.findUnique({
where: { id: req.user!.id },
select: { passwordHash: true }
});
if (!user) {
return res.status(404).json({
success: false,
error: 'User not found',
code: 'USER_NOT_FOUND'
});
}
// Verify current password
const isValidPassword = await AuthService.verifyPassword(currentPassword, user.passwordHash);
if (!isValidPassword) {
return res.status(401).json({
success: false,
error: 'Current password is incorrect',
code: 'INVALID_CREDENTIALS'
});
}
// Hash new password
const newPasswordHash = await AuthService.hashPassword(newPassword);
// Update password
await prisma.user.update({
where: { id: req.user!.id },
data: { passwordHash: newPasswordHash }
});
// Invalidate all existing sessions
AuthService.revokeAllUserTokens(req.user!.id);
await prisma.userSession.deleteMany({
where: { userId: req.user!.id }
});
res.json({
success: true,
message: 'Password changed successfully. Please log in again.'
});
} catch (error) {
next(error);
}
});
export default router;
// Get active sessions
router.get('/sessions', authenticateToken, async (req, res, next) => {
try {
const sessions = AuthService.getUserActiveSessions(req.user!.id);
res.json({
success: true,
data: {
sessions: sessions.map(session => ({
sessionId: session.sessionId,
expiresAt: session.expiresAt,
isCurrentSession: false // TODO: Identify current session
}))
}
});
} catch (error) {
next(error);
}
});
// Revoke specific session
router.delete('/sessions/:sessionId', authenticateToken, async (req, res, next) => {
try {
const { sessionId } = req.params;
// Revoke specific session
AuthService.revokeUserSession(req.user!.id, sessionId);
res.json({
success: true,
message: 'Session revoked successfully'
});
} catch (error) {
next(error);
}
});
// Validate token endpoint (for debugging/testing)
router.post('/validate', async (req, res, next) => {
try {
const { token } = req.body;
if (!token) {
return res.status(400).json({
success: false,
error: 'Token is required',
code: 'VALIDATION_ERROR'
});
}
const payload = AuthService.verifyToken(token);
res.json({
success: true,
data: {
valid: true,
payload: {
userId: payload.userId,
email: payload.email,
subscription: payload.subscription,
type: payload.type,
sessionId: payload.sessionId,
expiresAt: new Date(payload.exp! * 1000)
}
}
});
} catch (error) {
res.json({
success: true,
data: {
valid: false,
error: error instanceof Error ? error.message : 'Invalid token'
}
});
}
});
+41
View File
@@ -0,0 +1,41 @@
import express from 'express';
import authRoutes from './auth';
import projectRoutes from './projects';
import rbacRoutes from './rbac';
import { apiRateLimitMiddleware } from '../middleware/security';
const router = express.Router();
// Health check endpoint
router.get('/health', (req, res) => {
res.json({
success: true,
data: {
status: 'healthy',
timestamp: new Date().toISOString(),
version: '1.0.0'
}
});
});
// API version info
router.get('/version', (req, res) => {
res.json({
success: true,
data: {
version: '1.0.0',
apiVersion: 'v1',
environment: process.env.NODE_ENV || 'development'
}
});
});
// Apply API rate limiting to all API routes
router.use('/api', apiRateLimitMiddleware);
// Mount route modules
router.use('/api/auth', authRoutes);
router.use('/api/projects', projectRoutes);
router.use('/api/rbac', rbacRoutes);
export default router;
+381
View File
@@ -0,0 +1,381 @@
import express from 'express';
// import { prisma } from '@cloneweb/database';
// import { validateProjectCreation, validatePagination } from '@cloneweb/shared';
import { authenticateToken, requireSubscription } from '../middleware/auth';
import { requirePermission, requireProjectAccess, AuthorizedRequest, addUserPermissions } from '../middleware/authorization';
import { RBACService } from '../services/rbac.service';
// Temporary mock implementations
const prisma = {
project: {
findMany: async (params: any): Promise<any[]> => [],
count: async (params: any): Promise<number> => 0,
create: async (params: any): Promise<any> => ({
id: 'mock-project-id',
name: params.data.name,
description: params.data.description,
sourceUrl: params.data.sourceUrl,
status: 'CREATED',
createdAt: new Date(),
settings: params.data.settings || {}
}),
findFirst: async (params: any): Promise<any> => ({
id: 'mock-project-id',
name: 'Mock Project',
status: 'CREATED',
user: { id: 'mock-user-id', email: 'mock@example.com' }
}),
update: async (params: any): Promise<any> => ({
id: params.where.id,
name: 'Updated Project',
updatedAt: new Date()
}),
delete: async (params: any): Promise<any> => ({ id: params.where.id })
}
};
const validateProjectCreation = (data: any) => {
if (!data.name || !data.sourceUrl) {
throw new Error('Name and source URL are required');
}
return data;
};
const validatePagination = (data: any) => ({
page: parseInt(data.page) || 1,
limit: parseInt(data.limit) || 20
});
const router = express.Router();
// Apply authentication and user permissions to all routes
router.use(authenticateToken);
router.use(addUserPermissions('id'));
// Get user's projects
router.get('/', async (req: AuthorizedRequest, res, next) => {
try {
const pagination = validatePagination(req.query);
const skip = (pagination.page - 1) * pagination.limit;
const [projects, total] = await Promise.all([
prisma.project.findMany({
where: { userId: req.user!.id },
select: {
id: true,
name: true,
description: true,
sourceUrl: true,
status: true,
createdAt: true,
updatedAt: true,
_count: {
select: {
pages: true,
assets: true,
components: true
}
}
},
orderBy: { updatedAt: 'desc' },
skip,
take: pagination.limit
}),
prisma.project.count({
where: { userId: req.user!.id }
})
]);
const totalPages = Math.ceil(total / pagination.limit);
res.json({
success: true,
data: projects,
pagination: {
page: pagination.page,
limit: pagination.limit,
total,
totalPages
}
});
} catch (error) {
next(error);
}
});
// Create new project
router.post('/', requirePermission('project:create'), async (req: AuthorizedRequest, res, next) => {
try {
const validatedData = validateProjectCreation(req.body);
// Check subscription limits
const userProjects = await prisma.project.count({
where: { userId: req.user!.id }
});
const limits = {
FREE: 3,
BASIC: 10,
PRO: 50,
ENTERPRISE: -1 // unlimited
};
const userLimit = limits[req.user!.subscription as keyof typeof limits];
if (userLimit !== -1 && userProjects >= userLimit) {
return res.status(403).json({
success: false,
error: `Project limit reached for ${req.user!.subscription} subscription`,
code: 'PROJECT_LIMIT_EXCEEDED'
});
}
const project = await prisma.project.create({
data: {
name: validatedData.name,
description: validatedData.description,
sourceUrl: validatedData.sourceUrl,
userId: req.user!.id,
settings: validatedData.settings || {},
analytics: {
create: {
totalPages: 0,
totalAssets: 0,
totalComponents: 0
}
}
},
select: {
id: true,
name: true,
description: true,
sourceUrl: true,
status: true,
createdAt: true,
settings: true
}
});
// Assign project owner role to the creator
RBACService.assignRole(req.user!.id, 'project-owner', project.id, req.user!.id);
res.status(201).json({
success: true,
data: project,
message: 'Project created successfully',
userPermissions: (req as any).userPermissions
});
} catch (error) {
next(error);
}
});
// Get project by ID
router.get('/:id', requireProjectAccess('read', 'id'), async (req: AuthorizedRequest, res, next) => {
try {
const project = await prisma.project.findFirst({
where: {
id: req.params.id,
OR: [
{ userId: req.user!.id },
{
collaborations: {
some: { userId: req.user!.id }
}
}
]
},
select: {
id: true,
name: true,
description: true,
sourceUrl: true,
status: true,
settings: true,
createdAt: true,
updatedAt: true,
user: {
select: {
id: true,
email: true,
firstName: true,
lastName: true
}
},
analytics: {
select: {
cloneAccuracy: true,
processingTime: true,
totalPages: true,
totalAssets: true,
totalComponents: true,
lastUpdated: true
}
},
_count: {
select: {
pages: true,
assets: true,
components: true,
collaborations: true
}
}
}
});
if (!project) {
return res.status(404).json({
success: false,
error: 'Project not found',
code: 'PROJECT_NOT_FOUND'
});
}
res.json({
success: true,
data: project
});
} catch (error) {
next(error);
}
});
// Update project
router.patch('/:id', requireProjectAccess('update', 'id'), async (req: AuthorizedRequest, res, next) => {
try {
const { name, description, settings } = req.body;
// Check if user owns the project
const existingProject = await prisma.project.findFirst({
where: {
id: req.params.id,
userId: req.user!.id
}
});
if (!existingProject) {
return res.status(404).json({
success: false,
error: 'Project not found or access denied',
code: 'PROJECT_NOT_FOUND'
});
}
const updatedProject = await prisma.project.update({
where: { id: req.params.id },
data: {
...(name && { name }),
...(description !== undefined && { description }),
...(settings && { settings })
},
select: {
id: true,
name: true,
description: true,
sourceUrl: true,
status: true,
settings: true,
updatedAt: true
}
});
res.json({
success: true,
data: updatedProject,
message: 'Project updated successfully'
});
} catch (error) {
next(error);
}
});
// Delete project
router.delete('/:id', requireProjectAccess('delete', 'id'), async (req: AuthorizedRequest, res, next) => {
try {
// Check if user owns the project
const existingProject = await prisma.project.findFirst({
where: {
id: req.params.id,
userId: req.user!.id
}
});
if (!existingProject) {
return res.status(404).json({
success: false,
error: 'Project not found or access denied',
code: 'PROJECT_NOT_FOUND'
});
}
// Delete project (cascade will handle related records)
await prisma.project.delete({
where: { id: req.params.id }
});
// Remove all project-specific role assignments
const projectMembers = RBACService.getProjectMembers(req.params.id);
projectMembers.forEach(member => {
member.roles.forEach(role => {
RBACService.removeRole(member.userId, role.roleId, req.params.id);
});
});
res.json({
success: true,
message: 'Project deleted successfully'
});
} catch (error) {
next(error);
}
});
// Start project cloning
router.post('/:id/clone', requirePermission('crawl:start', 'id'), requireSubscription('BASIC'), async (req: AuthorizedRequest, res, next) => {
try {
const project = await prisma.project.findFirst({
where: {
id: req.params.id,
userId: req.user!.id
}
});
if (!project) {
return res.status(404).json({
success: false,
error: 'Project not found',
code: 'PROJECT_NOT_FOUND'
});
}
if (project.status === 'CRAWLING' || project.status === 'PROCESSING') {
return res.status(400).json({
success: false,
error: 'Project is already being processed',
code: 'PROJECT_ALREADY_PROCESSING'
});
}
// Update project status
await prisma.project.update({
where: { id: req.params.id },
data: { status: 'CRAWLING' }
});
// TODO: Queue crawling job
// This would typically send a message to the crawler service
res.json({
success: true,
message: 'Cloning process started',
data: {
projectId: project.id,
status: 'CRAWLING'
}
});
} catch (error) {
next(error);
}
});
export default router;
+381
View File
@@ -0,0 +1,381 @@
import { Router } from 'express';
import { RBACService, Role, Permission } from '../services/rbac.service';
import { requirePermission, requireAdmin, AuthorizedRequest, getUserPermissionsForResponse } from '../middleware/authorization';
import { authenticateToken } from '../middleware/auth';
const router = Router();
// Apply authentication to all RBAC routes
router.use(authenticateToken);
/**
* Get all permissions
*/
router.get('/permissions', requirePermission('admin:roles'), (req, res) => {
try {
const permissions = RBACService.getAllPermissions();
res.json({
success: true,
permissions: permissions.map(p => ({
id: p.id,
name: p.name,
description: p.description,
resource: p.resource,
action: p.action
}))
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch permissions'
});
}
});
/**
* Get all roles
*/
router.get('/roles', requirePermission('admin:roles'), (req, res) => {
try {
const roles = RBACService.getAllRoles();
res.json({
success: true,
roles: roles.map(r => ({
id: r.id,
name: r.name,
description: r.description,
permissions: r.permissions,
isSystemRole: r.isSystemRole
}))
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch roles'
});
}
});
/**
* Get specific role
*/
router.get('/roles/:roleId', requirePermission('admin:roles'), (req, res) => {
try {
const { roleId } = req.params;
const role = RBACService.getRole(roleId);
if (!role) {
return res.status(404).json({
success: false,
error: 'Role not found'
});
}
res.json({
success: true,
role: {
id: role.id,
name: role.name,
description: role.description,
permissions: role.permissions,
isSystemRole: role.isSystemRole
}
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch role'
});
}
});
/**
* Create new role
*/
router.post('/roles', requirePermission('admin:roles'), (req, res) => {
try {
const { id, name, description, permissions } = req.body;
if (!id || !name || !Array.isArray(permissions)) {
return res.status(400).json({
success: false,
error: 'Missing required fields: id, name, permissions'
});
}
const role: Role = {
id,
name,
description: description || '',
permissions,
isSystemRole: false
};
const created = RBACService.createRole(role);
if (!created) {
return res.status(409).json({
success: false,
error: 'Role already exists or invalid permissions'
});
}
res.status(201).json({
success: true,
message: 'Role created successfully',
role: {
id: role.id,
name: role.name,
description: role.description,
permissions: role.permissions,
isSystemRole: role.isSystemRole
}
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to create role'
});
}
});
/**
* Create new permission
*/
router.post('/permissions', requirePermission('admin:roles'), (req, res) => {
try {
const { id, name, description, resource, action } = req.body;
if (!id || !name || !resource || !action) {
return res.status(400).json({
success: false,
error: 'Missing required fields: id, name, resource, action'
});
}
const permission: Permission = {
id,
name,
description: description || '',
resource,
action
};
const created = RBACService.createPermission(permission);
if (!created) {
return res.status(409).json({
success: false,
error: 'Permission already exists'
});
}
res.status(201).json({
success: true,
message: 'Permission created successfully',
permission: {
id: permission.id,
name: permission.name,
description: permission.description,
resource: permission.resource,
action: permission.action
}
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to create permission'
});
}
});
/**
* Assign role to user
*/
router.post('/users/:userId/roles', requirePermission('admin:users'), (req, res) => {
try {
const { userId } = req.params;
const { roleId, projectId } = req.body;
const grantedBy = (req as AuthorizedRequest).user?.id;
if (!roleId) {
return res.status(400).json({
success: false,
error: 'Role ID is required'
});
}
const assigned = RBACService.assignRole(userId, roleId, projectId, grantedBy);
if (!assigned) {
return res.status(409).json({
success: false,
error: 'Role assignment failed - role may not exist or user already has this role'
});
}
res.json({
success: true,
message: 'Role assigned successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to assign role'
});
}
});
/**
* Remove role from user
*/
router.delete('/users/:userId/roles/:roleId', requirePermission('admin:users'), (req, res) => {
try {
const { userId, roleId } = req.params;
const { projectId } = req.query;
const removed = RBACService.removeRole(userId, roleId, projectId as string);
if (!removed) {
return res.status(404).json({
success: false,
error: 'Role assignment not found'
});
}
res.json({
success: true,
message: 'Role removed successfully'
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to remove role'
});
}
});
/**
* Get user's roles and permissions
*/
router.get('/users/:userId/permissions', requirePermission('admin:users'), (req, res) => {
try {
const { userId } = req.params;
const { projectId } = req.query;
const userPermissions = getUserPermissionsForResponse(userId, projectId as string);
res.json({
success: true,
userId,
projectId: projectId || null,
...userPermissions
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch user permissions'
});
}
});
/**
* Get current user's permissions
*/
router.get('/me/permissions', (req, res) => {
try {
const userId = (req as AuthorizedRequest).user?.id;
const { projectId } = req.query;
if (!userId) {
return res.status(401).json({
success: false,
error: 'Authentication required'
});
}
const userPermissions = getUserPermissionsForResponse(userId, projectId as string);
res.json({
success: true,
userId,
projectId: projectId || null,
...userPermissions
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch permissions'
});
}
});
/**
* Check if user has specific permission
*/
router.post('/check-permission', (req, res) => {
try {
const userId = (req as AuthorizedRequest).user?.id;
const { permissionId, projectId } = req.body;
if (!userId) {
return res.status(401).json({
success: false,
error: 'Authentication required'
});
}
if (!permissionId) {
return res.status(400).json({
success: false,
error: 'Permission ID is required'
});
}
const hasPermission = RBACService.hasPermission(userId, permissionId, projectId);
res.json({
success: true,
hasPermission,
userId,
permissionId,
projectId: projectId || null
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to check permission'
});
}
});
/**
* Get project members
*/
router.get('/projects/:projectId/members', requirePermission('project:read', 'projectId'), (req, res) => {
try {
const { projectId } = req.params;
const members = RBACService.getProjectMembers(projectId);
res.json({
success: true,
projectId,
members: members.map(member => ({
userId: member.userId,
roles: member.roles.map(role => ({
roleId: role.roleId,
grantedAt: role.grantedAt,
grantedBy: role.grantedBy
}))
}))
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Failed to fetch project members'
});
}
});
export default router;