PRIMEIRO ENVIO
This commit is contained in:
@@ -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'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user