realtimecursor
Version:
Real-time collaboration system with cursor tracking and approval workflow
1,550 lines (1,323 loc) • 44.6 kB
JavaScript
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-this-in-production';
const SALT_ROUNDS = 12;
class SimpleAuthService {
constructor() {
// In-memory user storage (replace with database in production)
this.users = new Map();
this.projects = new Map();
this.invitations = new Map();
this.stagedChanges = new Map();
this.comments = new Map();
this.projectVersions = new Map();
this.projectAnalytics = new Map();
this.activityHistory = new Map();
this.editHistory = new Map();
console.log('✅ SimpleAuthService initialized successfully');
// Initialize default admin user
setTimeout(() => {
this.initializeDefaultAdmin();
}, 100);
}
// Generate JWT token
generateJWT(payload, expiresIn = '24h') {
return jwt.sign(payload, JWT_SECRET, { expiresIn });
}
// Verify JWT token
verifyJWT(token) {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
throw new Error('Invalid or expired token');
}
}
// Hash password
async hashPassword(password) {
try {
const salt = await bcrypt.genSalt(SALT_ROUNDS);
return await bcrypt.hash(password, salt);
} catch (error) {
throw new Error('Password hashing failed');
}
}
// Verify password
async verifyPassword(password, hashedPassword) {
try {
return await bcrypt.compare(password, hashedPassword);
} catch (error) {
throw new Error('Password verification failed');
}
}
// Validate email format
validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// Validate password strength
validatePassword(password) {
if (password.length < 8) {
return { valid: false, message: 'Password must be at least 8 characters long' };
}
if (!/(?=.*[a-z])/.test(password)) {
return { valid: false, message: 'Password must contain at least one lowercase letter' };
}
if (!/(?=.*[A-Z])/.test(password)) {
return { valid: false, message: 'Password must contain at least one uppercase letter' };
}
if (!/(?=.*\d)/.test(password)) {
return { valid: false, message: 'Password must contain at least one number' };
}
if (!/(?=.*[@$!%*?&])/.test(password)) {
return { valid: false, message: 'Password must contain at least one special character' };
}
return { valid: true };
}
// Register new user
async register(userData, isAdminCreated = false) {
try {
const { email, password, firstName, lastName, role = 'user' } = userData;
console.log('Registration attempt with data:', { email, firstName, lastName, role });
// Validate input
if (!email || !password || !firstName || !lastName) {
throw new Error('All fields are required');
}
// Validate email format
if (!this.validateEmail(email)) {
throw new Error('Invalid email format');
}
// Validate password strength
const passwordValidation = this.validatePassword(password);
if (!passwordValidation.valid) {
throw new Error(passwordValidation.message);
}
// Check if user already exists
const normalizedEmail = email.toLowerCase();
if (this.users.has(normalizedEmail)) {
throw new Error('User already exists with this email');
}
// Hash password
const hashedPassword = await this.hashPassword(password);
// Create user
const userId = crypto.randomUUID();
const userRole = ['admin', 'superadmin'].includes(role) ? role : 'user';
const user = {
id: userId,
email: normalizedEmail,
password: hashedPassword,
firstName: firstName.trim(),
lastName: lastName.trim(),
fullName: `${firstName.trim()} ${lastName.trim()}`,
role: userRole,
emailConfirmed: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
createdBy: isAdminCreated ? 'admin' : 'self'
};
// Store user
this.users.set(normalizedEmail, user);
// Generate token
const token = this.generateJWT({
sub: userId,
email: normalizedEmail,
role: userRole,
aud: 'authenticated'
});
console.log('User registered successfully:', { email: user.email, firstName: user.firstName, role: user.role });
return {
success: true,
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
fullName: user.fullName,
role: user.role,
emailConfirmed: user.emailConfirmed,
createdAt: user.createdAt
},
token
};
} catch (error) {
console.error('Registration error:', error.message);
throw error;
}
}
// Login user
async login(email, password) {
try {
console.log('Login attempt for email:', email);
if (!email || !password) {
throw new Error('Email and password are required');
}
if (!this.validateEmail(email)) {
throw new Error('Invalid email format');
}
// Get user
const normalizedEmail = email.toLowerCase();
const user = this.users.get(normalizedEmail);
if (!user) {
throw new Error('Invalid credentials');
}
// Verify password
const isValidPassword = await this.verifyPassword(password, user.password);
if (!isValidPassword) {
throw new Error('Invalid credentials');
}
// Update last login time
user.lastLoginAt = new Date().toISOString();
// Generate token
const token = this.generateJWT({
sub: user.id,
email: user.email,
role: user.role || 'user',
aud: 'authenticated'
});
console.log('Login successful for user:', user.email, 'with role:', user.role);
return {
success: true,
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
fullName: user.fullName,
role: user.role || 'user',
emailConfirmed: user.emailConfirmed,
createdAt: user.createdAt
},
token
};
} catch (error) {
console.error('Login error:', error.message);
throw error;
}
}
// Get user by ID
async getUserById(userId) {
for (const user of this.users.values()) {
if (user.id === userId) {
return user;
}
}
return null;
}
// Get user by email
async getUserByEmail(email) {
return this.users.get(email.toLowerCase()) || null;
}
// Update user
async updateUser(userId, updateData) {
try {
const user = await this.getUserById(userId);
if (!user) {
throw new Error('User not found');
}
const { firstName, lastName, email } = updateData;
if (firstName) user.firstName = firstName.trim();
if (lastName) user.lastName = lastName.trim();
if (firstName || lastName) {
user.fullName = `${user.firstName} ${user.lastName}`;
}
if (email && email !== user.email) {
const normalizedEmail = email.toLowerCase();
if (this.users.has(normalizedEmail)) {
throw new Error('Email already in use');
}
// Update email key in map
this.users.delete(user.email);
user.email = normalizedEmail;
this.users.set(normalizedEmail, user);
}
user.updatedAt = new Date().toISOString();
return {
success: true,
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
fullName: user.fullName,
emailConfirmed: user.emailConfirmed,
createdAt: user.createdAt
}
};
} catch (error) {
console.error('Update user error:', error.message);
throw error;
}
}
// Change password
async changePassword(userId, currentPassword, newPassword) {
try {
const user = await this.getUserById(userId);
if (!user) {
throw new Error('User not found');
}
// Verify current password
const isValidPassword = await this.verifyPassword(currentPassword, user.password);
if (!isValidPassword) {
throw new Error('Current password is incorrect');
}
// Validate new password
const passwordValidation = this.validatePassword(newPassword);
if (!passwordValidation.valid) {
throw new Error(passwordValidation.message);
}
// Hash new password
const hashedPassword = await this.hashPassword(newPassword);
user.password = hashedPassword;
user.updatedAt = new Date().toISOString();
return { success: true, message: 'Password updated successfully' };
} catch (error) {
console.error('Change password error:', error.message);
throw error;
}
}
// Delete user
async deleteUser(userId) {
try {
const user = await this.getUserById(userId);
if (!user) {
throw new Error('User not found');
}
this.users.delete(user.email);
return { success: true, message: 'User deleted successfully' };
} catch (error) {
console.error('Delete user error:', error.message);
throw error;
}
}
// Get all users (admin only)
async getAllUsers(filters = {}) {
try {
let users = Array.from(this.users.values()).map(user => ({
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
fullName: user.fullName,
role: user.role || 'user',
emailConfirmed: user.emailConfirmed,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
createdBy: user.createdBy || 'self',
lastLoginAt: user.lastLoginAt || null
}));
// Apply filters
if (filters.role) {
users = users.filter(user => user.role === filters.role);
}
if (filters.search) {
const searchTerm = filters.search.toLowerCase();
users = users.filter(user =>
user.email.toLowerCase().includes(searchTerm) ||
user.fullName.toLowerCase().includes(searchTerm)
);
}
// Sort by creation date (newest first)
users.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
return { success: true, users, total: users.length };
} catch (error) {
console.error('Get all users error:', error.message);
throw error;
}
}
// Create user by admin
async createUserByAdmin(userData) {
try {
return await this.register(userData, true);
} catch (error) {
console.error('Create user by admin error:', error.message);
throw error;
}
}
// Update user role (admin only)
async updateUserRole(userId, newRole) {
try {
const user = await this.getUserById(userId);
if (!user) {
throw new Error('User not found');
}
if (!['user', 'admin', 'superadmin'].includes(newRole)) {
throw new Error('Invalid role. Must be "user", "admin", or "superadmin"');
}
user.role = newRole;
user.updatedAt = new Date().toISOString();
console.log(`User role updated: ${user.email} -> ${newRole}`);
return {
success: true,
message: `User role updated to ${newRole}`,
user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
fullName: user.fullName,
role: user.role,
emailConfirmed: user.emailConfirmed,
createdAt: user.createdAt
}
};
} catch (error) {
console.error('Update user role error:', error.message);
throw error;
}
}
// Change user password (admin/superadmin only)
async changeUserPassword(userId, newPassword) {
try {
const user = await this.getUserById(userId);
if (!user) {
throw new Error('User not found');
}
// Validate new password
const passwordValidation = this.validatePassword(newPassword);
if (!passwordValidation.valid) {
throw new Error(passwordValidation.message);
}
// Hash new password
const hashedPassword = await this.hashPassword(newPassword);
user.password = hashedPassword;
user.updatedAt = new Date().toISOString();
console.log(`Password changed for user: ${user.email}`);
return { success: true, message: 'Password updated successfully' };
} catch (error) {
console.error('Change user password error:', error.message);
throw error;
}
}
// Bulk delete users (admin only)
async bulkDeleteUsers(userIds) {
try {
const deletedUsers = [];
const errors = [];
for (const userId of userIds) {
try {
const user = await this.getUserById(userId);
if (user) {
this.users.delete(user.email);
deletedUsers.push(user.email);
}
} catch (error) {
errors.push({ userId, error: error.message });
}
}
return {
success: true,
message: `${deletedUsers.length} users deleted successfully`,
deletedUsers,
errors
};
} catch (error) {
console.error('Bulk delete users error:', error.message);
throw error;
}
}
// Get user statistics (admin only)
async getUserStats() {
try {
const users = Array.from(this.users.values());
const totalUsers = users.length;
const adminUsers = users.filter(user => user.role === 'admin').length;
const superAdminUsers = users.filter(user => user.role === 'superadmin').length;
const regularUsers = users.filter(user => user.role === 'user').length;
const recentUsers = users.filter(user => {
const createdDate = new Date(user.createdAt);
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
return createdDate > weekAgo;
}).length;
return {
success: true,
stats: {
totalUsers,
adminUsers: adminUsers + superAdminUsers,
regularUsers,
recentUsers,
superAdminUsers
}
};
} catch (error) {
console.error('Get user stats error:', error.message);
throw error;
}
}
// Check if user is admin or superadmin
isAdmin(userId) {
const user = Array.from(this.users.values()).find(u => u.id === userId);
return user && (user.role === 'admin' || user.role === 'superadmin');
}
// Check if user is superadmin
isSuperAdmin(userId) {
const user = Array.from(this.users.values()).find(u => u.id === userId);
return user && user.role === 'superadmin';
}
// Admin middleware
requireAdmin(req, res, next) {
this.authenticateToken(req, res, () => {
if (!this.isAdmin(req.user.sub)) {
return res.status(403).json({
success: false,
message: 'Admin access required'
});
}
next();
});
}
// Initialize default admin user
async initializeDefaultAdmin() {
try {
const adminEmail = 'admin@example.com';
const superAdminEmail = 'superadmin@example.com';
if (!this.users.has(adminEmail)) {
await this.register({
email: adminEmail,
password: 'Admin123!',
firstName: 'System',
lastName: 'Administrator',
role: 'admin'
}, true);
console.log('✅ Default admin user created:', adminEmail, 'password: Admin123!');
}
if (!this.users.has(superAdminEmail)) {
await this.register({
email: superAdminEmail,
password: 'SuperAdmin123!',
firstName: 'Super',
lastName: 'Administrator',
role: 'superadmin'
}, true);
console.log('✅ Default super admin user created:', superAdminEmail, 'password: SuperAdmin123!');
}
} catch (error) {
console.error('Failed to create default admin:', error.message);
}
}
// Middleware to authenticate requests
authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
const decoded = this.verifyJWT(token);
req.user = decoded;
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
}
// Check if user is authenticated
isAuthenticated(token) {
try {
const decoded = this.verifyJWT(token);
return { authenticated: true, user: decoded };
} catch (error) {
return { authenticated: false, error: error.message };
}
}
// Project Management Methods
// Create project
async createProject(creatorId, projectData) {
try {
const { name, description } = projectData;
const creator = await this.getUserById(creatorId);
if (!creator) {
throw new Error('Creator not found');
}
const projectId = crypto.randomUUID();
const project = {
id: projectId,
name: name.trim(),
description: description.trim(),
createdBy: creatorId,
createdByName: creator.fullName,
members: [creatorId],
memberNames: [creator.fullName],
content: '# Welcome to the Project\n\nStart collaborating here...',
stagedContent: null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
lastActivity: new Date().toISOString()
};
this.projects.set(projectId, project);
console.log('Project created:', project.name, 'by', creator.fullName);
return {
success: true,
project: {
id: project.id,
name: project.name,
description: project.description,
createdBy: project.createdBy,
createdByName: project.createdByName,
members: project.members,
memberNames: project.memberNames,
createdAt: project.createdAt
}
};
} catch (error) {
console.error('Create project error:', error.message);
throw error;
}
}
// Get user projects
async getUserProjects(userId) {
try {
const projects = Array.from(this.projects.values())
.filter(project => project.members.includes(userId))
.map(project => {
// Get pending invitations count for this project
const pendingInvitations = Array.from(this.invitations.values())
.filter(inv => inv.projectId === project.id && inv.status === 'pending').length;
return {
id: project.id,
name: project.name,
description: project.description,
createdBy: project.createdBy,
createdByName: project.createdByName,
members: project.members,
memberNames: project.memberNames,
createdAt: project.createdAt,
lastActivity: project.lastActivity,
hasStagedChanges: project.stagedContent !== null,
pendingInvitations
};
});
return { success: true, projects };
} catch (error) {
console.error('Get user projects error:', error.message);
throw error;
}
}
// Get project by ID
async getProject(projectId, userId) {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
if (!project.members.includes(userId)) {
throw new Error('Access denied to this project');
}
const user = await this.getUserById(userId);
const isAdmin = user && (user.role === 'admin' || user.role === 'superadmin');
return {
success: true,
project: {
id: project.id,
name: project.name,
description: project.description,
createdBy: project.createdBy,
createdByName: project.createdByName,
members: project.members,
memberNames: project.memberNames,
content: project.content,
stagedContent: isAdmin ? project.stagedContent : null,
createdAt: project.createdAt,
lastActivity: project.lastActivity,
hasStagedChanges: project.stagedContent !== null,
canEdit: isAdmin || project.createdBy === userId
}
};
} catch (error) {
console.error('Get project error:', error.message);
throw error;
}
}
// Update project content
async updateProjectContent(projectId, userId, content) {
try {
console.log('Updating project content:', { projectId, userId, contentLength: content?.length });
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
if (!project.members.includes(userId)) {
throw new Error('Access denied to this project');
}
const user = await this.getUserById(userId);
if (!user) {
throw new Error('User not found');
}
const isAdmin = user && (user.role === 'admin' || user.role === 'superadmin');
const isOwner = project.createdBy === userId;
// Ensure content is a string
const safeContent = content !== undefined && content !== null ? String(content) : '';
if (isAdmin || isOwner) {
// Save to history before updating
this.saveEditHistory(projectId, userId, project.content, safeContent, 'direct_edit');
// Admin or owner can directly update content
project.content = safeContent;
project.updatedAt = new Date().toISOString();
project.lastActivity = new Date().toISOString();
console.log('Content updated directly by admin/owner');
return {
success: true,
message: 'Content updated successfully',
directUpdate: true
};
} else {
// Normal user - stage the changes
const changeId = crypto.randomUUID();
const stagedChange = {
id: changeId,
projectId: projectId,
userId: userId,
userName: user.fullName,
originalContent: project.content || '',
proposedContent: safeContent,
status: 'pending',
createdAt: new Date().toISOString(),
feedback: null
};
this.stagedChanges.set(changeId, stagedChange);
project.stagedContent = safeContent;
project.lastActivity = new Date().toISOString();
console.log('Content staged for approval');
return {
success: true,
message: 'Changes staged for admin approval',
staged: true,
changeId: changeId
};
}
} catch (error) {
console.error('Update project content error:', error.message);
throw error;
}
}
// Invite users to project
async inviteUsersToProject(projectId, inviterId, userIds) {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
const inviter = await this.getUserById(inviterId);
if (!inviter) {
throw new Error('Inviter not found');
}
const invitations = [];
for (const userId of userIds) {
const invitee = await this.getUserById(userId);
if (!invitee) continue;
if (project.members.includes(userId)) continue;
const invitationId = crypto.randomUUID();
const invitation = {
id: invitationId,
projectId: projectId,
projectName: project.name,
projectDescription: project.description,
inviterId: inviterId,
inviterName: inviter.fullName,
inviteeId: userId,
inviteeName: invitee.fullName,
inviteeEmail: invitee.email,
status: 'pending',
createdAt: new Date().toISOString()
};
this.invitations.set(invitationId, invitation);
invitations.push(invitation);
}
return {
success: true,
message: `${invitations.length} invitations sent`,
invitations: invitations.length
};
} catch (error) {
console.error('Invite users to project error:', error.message);
throw error;
}
}
// Get user invitations
async getUserInvitations(userId) {
try {
const invitations = Array.from(this.invitations.values())
.filter(inv => inv.inviteeId === userId && inv.status === 'pending')
.map(inv => ({
id: inv.id,
projectId: inv.projectId,
projectName: inv.projectName,
projectDescription: inv.projectDescription,
inviterName: inv.inviterName,
createdAt: inv.createdAt
}));
return { success: true, invitations };
} catch (error) {
console.error('Get user invitations error:', error.message);
throw error;
}
}
// Get project invitations (for superadmin)
async getProjectInvitations(projectId) {
try {
const invitations = Array.from(this.invitations.values())
.filter(inv => inv.projectId === projectId)
.map(inv => ({
id: inv.id,
inviteeName: inv.inviteeName,
inviteeEmail: inv.inviteeEmail,
status: inv.status,
createdAt: inv.createdAt,
respondedAt: inv.respondedAt
}));
return { success: true, invitations };
} catch (error) {
console.error('Get project invitations error:', error.message);
throw error;
}
}
// Respond to invitation
async respondToInvitation(invitationId, userId, accept) {
try {
const invitation = this.invitations.get(invitationId);
if (!invitation) {
throw new Error('Invitation not found');
}
if (invitation.inviteeId !== userId) {
throw new Error('Access denied to this invitation');
}
if (invitation.status !== 'pending') {
throw new Error('Invitation already responded to');
}
if (accept) {
const project = this.projects.get(invitation.projectId);
if (project) {
project.members.push(userId);
project.memberNames.push(invitation.inviteeName);
project.lastActivity = new Date().toISOString();
}
invitation.status = 'accepted';
} else {
invitation.status = 'declined';
}
invitation.respondedAt = new Date().toISOString();
return {
success: true,
message: accept ? 'Invitation accepted' : 'Invitation declined',
accepted: accept
};
} catch (error) {
console.error('Respond to invitation error:', error.message);
throw error;
}
}
// Get staged changes for project
async getStagedChanges(projectId) {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
const changes = Array.from(this.stagedChanges.values())
.filter(change => change.projectId === projectId && change.status === 'pending')
.map(change => ({
id: change.id,
userId: change.userId,
userName: change.userName,
originalContent: change.originalContent,
proposedContent: change.proposedContent,
createdAt: change.createdAt
}));
return { success: true, changes, projectName: project.name };
} catch (error) {
console.error('Get staged changes error:', error.message);
throw error;
}
}
// Review staged change
async reviewStagedChange(changeId, reviewerId, approve, feedback = null) {
try {
const change = this.stagedChanges.get(changeId);
if (!change) {
throw new Error('Staged change not found');
}
if (change.status !== 'pending') {
throw new Error('Change already reviewed');
}
const project = this.projects.get(change.projectId);
if (!project) {
throw new Error('Project not found');
}
const reviewer = await this.getUserById(reviewerId);
if (!reviewer) {
throw new Error('Reviewer not found');
}
if (approve) {
// Apply the staged changes
project.content = change.proposedContent;
project.stagedContent = null;
change.status = 'approved';
} else {
// Reject the changes
project.stagedContent = null;
change.status = 'rejected';
}
change.reviewedBy = reviewerId;
change.reviewerName = reviewer.fullName;
change.reviewedAt = new Date().toISOString();
change.feedback = feedback;
project.updatedAt = new Date().toISOString();
project.lastActivity = new Date().toISOString();
return {
success: true,
message: approve ? 'Changes approved and applied' : 'Changes rejected',
approved: approve
};
} catch (error) {
console.error('Review staged change error:', error.message);
throw error;
}
}
// Comment Management Methods
// Get project comments
async getProjectComments(projectId, userId) {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
if (!project.members.includes(userId)) {
throw new Error('Access denied to this project');
}
const comments = Array.from(this.comments?.values() || [])
.filter(comment => comment.projectId === projectId)
.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
return {
success: true,
comments
};
} catch (error) {
console.error('Get project comments error:', error.message);
throw error;
}
}
// Add comment
async addComment(projectId, userId, commentData) {
try {
console.log('Adding comment:', { projectId, userId, commentData });
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
if (!project.members.includes(userId)) {
throw new Error('Access denied to this project');
}
const user = await this.getUserById(userId);
if (!user) {
throw new Error('User not found');
}
if (!commentData.text || !commentData.selectedText) {
throw new Error('Comment text and selected text are required');
}
const commentId = crypto.randomUUID();
const comment = {
id: commentId,
projectId: projectId,
userId: userId,
userName: user.fullName,
text: commentData.text.trim(),
selectedText: commentData.selectedText.trim(),
startPosition: commentData.startPosition || 0,
endPosition: commentData.endPosition || commentData.selectedText.length,
createdAt: new Date().toISOString()
};
// Initialize comments map if it doesn't exist
if (!this.comments) {
this.comments = new Map();
}
this.comments.set(commentId, comment);
project.lastActivity = new Date().toISOString();
// Add to activity history
this.addActivityHistory(projectId, userId, 'comment_added', {
commentId,
text: commentData.text,
selectedText: commentData.selectedText
});
console.log('Comment added successfully:', comment);
return {
success: true,
comment,
message: 'Comment added successfully'
};
} catch (error) {
console.error('Add comment error:', error.message);
throw error;
}
}
// Delete comment
async deleteComment(projectId, commentId) {
try {
if (!this.comments) {
throw new Error('Comment not found');
}
const comment = this.comments.get(commentId);
if (!comment) {
throw new Error('Comment not found');
}
if (comment.projectId !== projectId) {
throw new Error('Comment does not belong to this project');
}
this.comments.delete(commentId);
return {
success: true,
message: 'Comment deleted successfully'
};
} catch (error) {
console.error('Delete comment error:', error.message);
throw error;
}
}
// Revolutionary Features
// Get project versions
async getProjectVersions(projectId, userId) {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
if (!project.members.includes(userId)) {
throw new Error('Access denied to this project');
}
const versions = Array.from(this.projectVersions?.values() || [])
.filter(version => version.projectId === projectId)
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
return {
success: true,
versions
};
} catch (error) {
console.error('Get project versions error:', error.message);
throw error;
}
}
// Get project analytics
async getProjectAnalytics(projectId, userId) {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
if (!project.members.includes(userId)) {
throw new Error('Access denied to this project');
}
const analytics = {
totalEdits: Math.floor(Math.random() * 100) + 20,
timeSpent: Math.floor(Math.random() * 300) + 60, // minutes
collaborators: project.members.length,
comments: Array.from(this.comments?.values() || [])
.filter(comment => comment.projectId === projectId).length,
productivity: ['Low', 'Medium', 'High'][Math.floor(Math.random() * 3)],
readabilityScore: Math.floor(Math.random() * 30) + 70,
engagementScore: Math.floor(Math.random() * 20) + 80
};
return {
success: true,
analytics
};
} catch (error) {
console.error('Get project analytics error:', error.message);
throw error;
}
}
// Generate AI suggestions
async generateAISuggestions(projectId, content, userId) {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
if (!project.members.includes(userId)) {
throw new Error('Access denied to this project');
}
// Mock AI suggestions
const suggestions = [
{
type: 'grammar',
text: 'Consider using active voice for better clarity',
position: Math.floor(Math.random() * content.length),
confidence: 0.85
},
{
type: 'style',
text: 'This sentence could be more concise',
position: Math.floor(Math.random() * content.length),
confidence: 0.72
},
{
type: 'content',
text: 'Add more details about implementation',
position: Math.floor(Math.random() * content.length),
confidence: 0.68
},
{
type: 'structure',
text: 'Consider adding subheadings for better organization',
position: Math.floor(Math.random() * content.length),
confidence: 0.91
}
];
return {
success: true,
suggestions: suggestions.slice(0, Math.floor(Math.random() * 3) + 1)
};
} catch (error) {
console.error('Generate AI suggestions error:', error.message);
throw error;
}
}
// Save project version
async saveProjectVersion(projectId, userId, content, versionName = null) {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
if (!project.members.includes(userId)) {
throw new Error('Access denied to this project');
}
const user = await this.getUserById(userId);
if (!user) {
throw new Error('User not found');
}
const versionId = crypto.randomUUID();
const version = {
id: versionId,
projectId: projectId,
userId: userId,
userName: user.fullName,
content: content,
name: versionName || `Version ${new Date().toLocaleString()}`,
createdAt: new Date().toISOString()
};
if (!this.projectVersions) {
this.projectVersions = new Map();
}
this.projectVersions.set(versionId, version);
return {
success: true,
version,
message: 'Version saved successfully'
};
} catch (error) {
console.error('Save project version error:', error.message);
throw error;
}
}
// Track project analytics
async trackProjectActivity(projectId, userId, activity) {
try {
if (!this.projectAnalytics) {
this.projectAnalytics = new Map();
}
const analyticsKey = `${projectId}-${userId}`;
let analytics = this.projectAnalytics.get(analyticsKey) || {
projectId,
userId,
activities: [],
totalTime: 0,
edits: 0,
lastActivity: null
};
analytics.activities.push({
type: activity.type,
timestamp: new Date().toISOString(),
data: activity.data
});
if (activity.type === 'edit') {
analytics.edits++;
}
analytics.lastActivity = new Date().toISOString();
this.projectAnalytics.set(analyticsKey, analytics);
return {
success: true,
message: 'Activity tracked successfully'
};
} catch (error) {
console.error('Track project activity error:', error.message);
throw error;
}
}
// History Tracking Methods
// Save edit history
saveEditHistory(projectId, userId, oldContent, newContent, editType = 'edit') {
try {
// Get user synchronously from existing data
let user = null;
for (const u of this.users.values()) {
if (u.id === userId) {
user = u;
break;
}
}
if (!user) {
console.warn('User not found for history:', userId);
return;
}
const historyId = crypto.randomUUID();
const historyEntry = {
id: historyId,
projectId,
userId,
userName: user.fullName,
editType,
oldContent: oldContent || '',
newContent: newContent || '',
timestamp: new Date().toISOString(),
changes: this.calculateChanges(oldContent || '', newContent || ''),
diff: this.generateDiff(oldContent || '', newContent || '')
};
if (!this.editHistory.has(projectId)) {
this.editHistory.set(projectId, []);
}
const projectHistory = this.editHistory.get(projectId);
projectHistory.push(historyEntry);
console.log('Edit history saved:', historyEntry);
// Keep only last 100 entries per project
if (projectHistory.length > 100) {
projectHistory.shift();
}
return historyEntry;
} catch (error) {
console.error('Save edit history error:', error);
}
}
// Add activity history
addActivityHistory(projectId, userId, activityType, data = {}) {
try {
// Get user synchronously
let user = null;
for (const u of this.users.values()) {
if (u.id === userId) {
user = u;
break;
}
}
if (!user) {
console.warn('User not found for activity:', userId);
return;
}
const activityId = crypto.randomUUID();
const activity = {
id: activityId,
projectId,
userId,
userName: user.fullName,
activityType,
data,
timestamp: new Date().toISOString()
};
if (!this.activityHistory.has(projectId)) {
this.activityHistory.set(projectId, []);
}
const projectActivities = this.activityHistory.get(projectId);
projectActivities.push(activity);
console.log('Activity history saved:', activity);
// Keep only last 200 activities per project
if (projectActivities.length > 200) {
projectActivities.shift();
}
return activity;
} catch (error) {
console.error('Add activity history error:', error);
}
}
// Calculate changes between two texts
calculateChanges(oldText, newText) {
const oldWords = oldText.split(/\s+/).filter(w => w.length > 0);
const newWords = newText.split(/\s+/).filter(w => w.length > 0);
return {
oldLength: oldText.length,
newLength: newText.length,
oldWordCount: oldWords.length,
newWordCount: newWords.length,
charactersAdded: Math.max(0, newText.length - oldText.length),
charactersRemoved: Math.max(0, oldText.length - newText.length),
wordsAdded: Math.max(0, newWords.length - oldWords.length),
wordsRemoved: Math.max(0, oldWords.length - newWords.length)
};
}
// Generate GitHub-style diff
generateDiff(oldText, newText) {
const oldLines = oldText.split('\n');
const newLines = newText.split('\n');
const diff = [];
let oldIndex = 0;
let newIndex = 0;
while (oldIndex < oldLines.length || newIndex < newLines.length) {
const oldLine = oldLines[oldIndex];
const newLine = newLines[newIndex];
if (oldIndex >= oldLines.length) {
// Only new lines left
diff.push({ type: 'added', content: newLine, lineNumber: newIndex + 1 });
newIndex++;
} else if (newIndex >= newLines.length) {
// Only old lines left
diff.push({ type: 'removed', content: oldLine, lineNumber: oldIndex + 1 });
oldIndex++;
} else if (oldLine === newLine) {
// Lines are the same
diff.push({ type: 'unchanged', content: oldLine, lineNumber: newIndex + 1 });
oldIndex++;
newIndex++;
} else {
// Lines are different
diff.push({ type: 'removed', content: oldLine, lineNumber: oldIndex + 1 });
diff.push({ type: 'added', content: newLine, lineNumber: newIndex + 1 });
oldIndex++;
newIndex++;
}
}
return diff;
}
// Get project history
async getProjectHistory(projectId, userId) {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
if (!project.members.includes(userId)) {
throw new Error('Access denied to this project');
}
const editHistory = this.editHistory.get(projectId) || [];
const activityHistory = this.activityHistory.get(projectId) || [];
// Combine and sort by timestamp
const allHistory = [
...editHistory.map(h => ({ ...h, type: 'edit' })),
...activityHistory.map(h => ({ ...h, type: 'activity' }))
].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
return {
success: true,
history: allHistory.slice(0, 50) // Return last 50 entries
};
} catch (error) {
console.error('Get project history error:', error.message);
throw error;
}
}
// Get detailed edit history
async getEditHistory(projectId, userId) {
try {
const project = this.projects.get(projectId);
if (!project) {
throw new Error('Project not found');
}
if (!project.members.includes(userId)) {
throw new Error('Access denied to this project');
}
const editHistory = this.editHistory.get(projectId) || [];
return {
success: true,
edits: editHistory.slice(-30).reverse() // Return last 30 edits, newest first
};
} catch (error) {
console.error('Get edit history error:', error.message);
throw error;
}
}
}
module.exports = SimpleAuthService;