diff --git a/backend/src/config/database.ts b/backend/src/config/database.ts index b65196d..3abf869 100644 --- a/backend/src/config/database.ts +++ b/backend/src/config/database.ts @@ -8,6 +8,8 @@ import { UserBadge } from '@models/UserBadge'; import { Badge } from '@models/Badge'; import { Scan } from '@models/Scan'; import { Report } from '@models/Report'; +import { CollaborationSession } from '@models/CollaborationSession'; +import { CollaborationParticipant } from '@models/CollaborationParticipant'; /** * Database Configuration @@ -32,7 +34,7 @@ export const sequelize = new Sequelize({ database: DB_NAME, username: DB_USER, password: DB_PASSWORD, - models: [User, Lab, LabInstance, UserProgress, UserBadge, Badge, Scan, Report], + models: [User, Lab, LabInstance, UserProgress, UserBadge, Badge, Scan, Report, CollaborationSession, CollaborationParticipant], logging: NODE_ENV === 'development' ? (msg) => logger.debug(msg) : false, pool: { min: parseInt(DB_POOL_MIN, 10), diff --git a/backend/src/controllers/CollaborationController.ts b/backend/src/controllers/CollaborationController.ts new file mode 100644 index 0000000..90fdfbf --- /dev/null +++ b/backend/src/controllers/CollaborationController.ts @@ -0,0 +1,590 @@ +import { Request, Response } from 'express'; +import { CollaborationSession } from '@models/CollaborationSession'; +import { CollaborationParticipant } from '@models/CollaborationParticipant'; +import { User } from '@models/User'; +import { Lab } from '@models/Lab'; +import { logger } from '@utils/logger'; +import { Op } from 'sequelize'; + +/** + * CollaborationController + * Handles collaboration session management + */ +export class CollaborationController { + /** + * GET /api/collaboration/sessions + * Get all collaboration sessions for the authenticated user + */ + static async getSessions(req: Request, res: Response): Promise { + try { + const userId = req.user?.id; + + if (!userId) { + res.status(401).json({ + success: false, + message: 'Unauthorized', + }); + return; + } + + // Get sessions where user is host or participant + const sessions = await CollaborationSession.findAll({ + where: { + [Op.or]: [ + { hostId: userId }, + { + '$participants.userId$': userId, + '$participants.isActive$': true, + }, + ], + }, + include: [ + { + model: User, + as: 'host', + attributes: ['id', 'username', 'email'], + }, + { + model: Lab, + as: 'lab', + attributes: ['id', 'title', 'description', 'difficulty'], + required: false, + }, + { + model: CollaborationParticipant, + as: 'participants', + where: { isActive: true }, + required: false, + include: [ + { + model: User, + as: 'user', + attributes: ['id', 'username', 'email'], + }, + ], + }, + ], + order: [['createdAt', 'DESC']], + }); + + res.json({ + success: true, + data: sessions, + }); + } catch (error) { + logger.error('Error fetching collaboration sessions:', error); + res.status(500).json({ + success: false, + message: 'Failed to fetch collaboration sessions', + }); + } + } + + /** + * GET /api/collaboration/sessions/active + * Get all active collaboration sessions (public) + */ + static async getActiveSessions(req: Request, res: Response): Promise { + try { + const sessions = await CollaborationSession.findAll({ + where: { status: 'active' }, + include: [ + { + model: User, + as: 'host', + attributes: ['id', 'username'], + }, + { + model: Lab, + as: 'lab', + attributes: ['id', 'title', 'difficulty'], + required: false, + }, + { + model: CollaborationParticipant, + as: 'participants', + where: { isActive: true }, + required: false, + attributes: ['userId', 'role'], + }, + ], + order: [['createdAt', 'DESC']], + }); + + res.json({ + success: true, + data: sessions, + }); + } catch (error) { + logger.error('Error fetching active sessions:', error); + res.status(500).json({ + success: false, + message: 'Failed to fetch active sessions', + }); + } + } + + /** + * GET /api/collaboration/sessions/:sessionId + * Get a specific collaboration session + */ + static async getSession(req: Request, res: Response): Promise { + try { + const { sessionId } = req.params; + const userId = req.user?.id; + + const session = await CollaborationSession.findByPk(sessionId, { + include: [ + { + model: User, + as: 'host', + attributes: ['id', 'username', 'email'], + }, + { + model: Lab, + as: 'lab', + attributes: ['id', 'title', 'description', 'difficulty'], + required: false, + }, + { + model: CollaborationParticipant, + as: 'participants', + where: { isActive: true }, + required: false, + include: [ + { + model: User, + as: 'user', + attributes: ['id', 'username', 'email'], + }, + ], + }, + ], + }); + + if (!session) { + res.status(404).json({ + success: false, + message: 'Session not found', + }); + return; + } + + // Check if user has access (host, participant, or admin) + const isHost = session.hostId === userId; + const isParticipant = session.participants?.some( + (p: any) => p.userId === userId && p.isActive + ); + const isAdmin = req.user?.role === 'admin'; + + if (!isHost && !isParticipant && !isAdmin) { + res.status(403).json({ + success: false, + message: 'Access denied', + }); + return; + } + + res.json({ + success: true, + data: session, + }); + } catch (error) { + logger.error('Error fetching session:', error); + res.status(500).json({ + success: false, + message: 'Failed to fetch session', + }); + } + } + + /** + * POST /api/collaboration/create + * Create a new collaboration session + */ + static async createSession(req: Request, res: Response): Promise { + try { + const userId = req.user?.id; + const { name, labId, maxParticipants } = req.body; + + if (!userId) { + res.status(401).json({ + success: false, + message: 'Unauthorized', + }); + return; + } + + // Validate lab exists if labId is provided + if (labId) { + const lab = await Lab.findByPk(labId); + if (!lab) { + res.status(404).json({ + success: false, + message: 'Lab not found', + }); + return; + } + } + + // Create session + const session = await CollaborationSession.create({ + name, + hostId: userId, + labId: labId || null, + maxParticipants: maxParticipants || 10, + status: 'active', + startedAt: new Date(), + }); + + // Add host as participant + await CollaborationParticipant.create({ + sessionId: session.id, + userId, + role: 'host', + isActive: true, + }); + + // Fetch complete session with associations + const completeSession = await CollaborationSession.findByPk(session.id, { + include: [ + { + model: User, + as: 'host', + attributes: ['id', 'username', 'email'], + }, + { + model: Lab, + as: 'lab', + attributes: ['id', 'title', 'description', 'difficulty'], + required: false, + }, + { + model: CollaborationParticipant, + as: 'participants', + where: { isActive: true }, + required: false, + include: [ + { + model: User, + as: 'user', + attributes: ['id', 'username', 'email'], + }, + ], + }, + ], + }); + + logger.info(`Collaboration session ${session.id} created by user ${userId}`); + + res.status(201).json({ + success: true, + data: completeSession, + message: 'Collaboration session created successfully', + }); + } catch (error) { + logger.error('Error creating collaboration session:', error); + res.status(500).json({ + success: false, + message: 'Failed to create collaboration session', + }); + } + } + + /** + * POST /api/collaboration/:sessionId/join + * Join an existing collaboration session + */ + static async joinSession(req: Request, res: Response): Promise { + try { + const { sessionId } = req.params; + const userId = req.user?.id; + + if (!userId) { + res.status(401).json({ + success: false, + message: 'Unauthorized', + }); + return; + } + + // Find session + const session = await CollaborationSession.findByPk(sessionId, { + include: [ + { + model: CollaborationParticipant, + as: 'participants', + where: { isActive: true }, + required: false, + }, + ], + }); + + if (!session) { + res.status(404).json({ + success: false, + message: 'Session not found', + }); + return; + } + + if (session.status !== 'active') { + res.status(400).json({ + success: false, + message: 'Session is not active', + }); + return; + } + + // Check if max participants reached + const activeParticipants = session.participants?.filter((p: any) => p.isActive) || []; + if (activeParticipants.length >= session.maxParticipants) { + res.status(400).json({ + success: false, + message: 'Session is full', + }); + return; + } + + // Check if user is already in session + const existingParticipant = await CollaborationParticipant.findOne({ + where: { + sessionId, + userId, + isActive: true, + }, + }); + + if (existingParticipant) { + res.status(400).json({ + success: false, + message: 'Already in session', + }); + return; + } + + // Add participant + await CollaborationParticipant.create({ + sessionId, + userId, + role: 'participant', + isActive: true, + }); + + // Fetch updated session + const updatedSession = await CollaborationSession.findByPk(sessionId, { + include: [ + { + model: User, + as: 'host', + attributes: ['id', 'username', 'email'], + }, + { + model: Lab, + as: 'lab', + attributes: ['id', 'title', 'description', 'difficulty'], + required: false, + }, + { + model: CollaborationParticipant, + as: 'participants', + where: { isActive: true }, + required: false, + include: [ + { + model: User, + as: 'user', + attributes: ['id', 'username', 'email'], + }, + ], + }, + ], + }); + + logger.info(`User ${userId} joined collaboration session ${sessionId}`); + + res.json({ + success: true, + data: updatedSession, + message: 'Joined session successfully', + }); + } catch (error) { + logger.error('Error joining collaboration session:', error); + res.status(500).json({ + success: false, + message: 'Failed to join collaboration session', + }); + } + } + + /** + * POST /api/collaboration/:sessionId/leave + * Leave a collaboration session + */ + static async leaveSession(req: Request, res: Response): Promise { + try { + const { sessionId } = req.params; + const userId = req.user?.id; + + if (!userId) { + res.status(401).json({ + success: false, + message: 'Unauthorized', + }); + return; + } + + // Find participant + const participant = await CollaborationParticipant.findOne({ + where: { + sessionId, + userId, + isActive: true, + }, + }); + + if (!participant) { + res.status(404).json({ + success: false, + message: 'Not in session', + }); + return; + } + + // Mark as inactive + await participant.update({ + isActive: false, + leftAt: new Date(), + }); + + // Find session + const session = await CollaborationSession.findByPk(sessionId); + + if (session) { + // If host left or no active participants, end session + if (session.hostId === userId) { + await session.update({ + status: 'ended', + endedAt: new Date(), + }); + + // Mark all participants as inactive + await CollaborationParticipant.update( + { + isActive: false, + leftAt: new Date(), + }, + { + where: { + sessionId, + isActive: true, + }, + } + ); + + logger.info(`Collaboration session ${sessionId} ended (host left)`); + } else { + // Check if any participants remain + const activeCount = await CollaborationParticipant.count({ + where: { + sessionId, + isActive: true, + }, + }); + + if (activeCount === 0) { + await session.update({ + status: 'ended', + endedAt: new Date(), + }); + logger.info(`Collaboration session ${sessionId} ended (no participants)`); + } + } + } + + logger.info(`User ${userId} left collaboration session ${sessionId}`); + + res.json({ + success: true, + message: 'Left session successfully', + }); + } catch (error) { + logger.error('Error leaving collaboration session:', error); + res.status(500).json({ + success: false, + message: 'Failed to leave collaboration session', + }); + } + } + + /** + * DELETE /api/collaboration/:sessionId + * End/delete a collaboration session (host only) + */ + static async endSession(req: Request, res: Response): Promise { + try { + const { sessionId } = req.params; + const userId = req.user?.id; + + if (!userId) { + res.status(401).json({ + success: false, + message: 'Unauthorized', + }); + return; + } + + const session = await CollaborationSession.findByPk(sessionId); + + if (!session) { + res.status(404).json({ + success: false, + message: 'Session not found', + }); + return; + } + + // Only host or admin can end session + if (session.hostId !== userId && req.user?.role !== 'admin') { + res.status(403).json({ + success: false, + message: 'Only the host can end this session', + }); + return; + } + + // End session + await session.update({ + status: 'ended', + endedAt: new Date(), + }); + + // Mark all participants as inactive + await CollaborationParticipant.update( + { + isActive: false, + leftAt: new Date(), + }, + { + where: { + sessionId, + isActive: true, + }, + } + ); + + logger.info(`Collaboration session ${sessionId} ended by user ${userId}`); + + res.json({ + success: true, + message: 'Session ended successfully', + }); + } catch (error) { + logger.error('Error ending collaboration session:', error); + res.status(500).json({ + success: false, + message: 'Failed to end collaboration session', + }); + } + } +} diff --git a/backend/src/database/migrations/010_create_collaboration_tables.ts b/backend/src/database/migrations/010_create_collaboration_tables.ts new file mode 100644 index 0000000..5f0c2f1 --- /dev/null +++ b/backend/src/database/migrations/010_create_collaboration_tables.ts @@ -0,0 +1,180 @@ +import { QueryInterface, DataTypes } from 'sequelize'; + +/** + * Migration: Create Collaboration Tables + * Creates tables for collaboration sessions and participants + */ +export async function up(queryInterface: QueryInterface): Promise { + // Create collaboration_sessions table + await queryInterface.createTable('collaboration_sessions', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.STRING(255), + allowNull: false, + }, + host_id: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'users', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + lab_id: { + type: DataTypes.UUID, + allowNull: true, + references: { + model: 'labs', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'SET NULL', + }, + status: { + type: DataTypes.ENUM('active', 'ended', 'scheduled'), + defaultValue: 'active', + allowNull: false, + }, + screen_sharing_user_id: { + type: DataTypes.UUID, + allowNull: true, + }, + screen_sharing_stream_id: { + type: DataTypes.STRING(255), + allowNull: true, + }, + max_participants: { + type: DataTypes.INTEGER, + defaultValue: 10, + allowNull: false, + }, + started_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + ended_at: { + type: DataTypes.DATE, + allowNull: true, + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + }); + + // Create collaboration_participants table + await queryInterface.createTable('collaboration_participants', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + session_id: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'collaboration_sessions', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + user_id: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'users', + key: 'id', + }, + onUpdate: 'CASCADE', + onDelete: 'CASCADE', + }, + role: { + type: DataTypes.ENUM('host', 'participant', 'viewer'), + defaultValue: 'participant', + allowNull: false, + }, + joined_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + left_at: { + type: DataTypes.DATE, + allowNull: true, + }, + is_active: { + type: DataTypes.BOOLEAN, + defaultValue: true, + allowNull: false, + }, + created_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + }, + }); + + // Add indexes for performance + await queryInterface.addIndex('collaboration_sessions', ['host_id'], { + name: 'idx_collab_sessions_host_id', + }); + + await queryInterface.addIndex('collaboration_sessions', ['lab_id'], { + name: 'idx_collab_sessions_lab_id', + }); + + await queryInterface.addIndex('collaboration_sessions', ['status'], { + name: 'idx_collab_sessions_status', + }); + + await queryInterface.addIndex('collaboration_participants', ['session_id'], { + name: 'idx_collab_participants_session_id', + }); + + await queryInterface.addIndex('collaboration_participants', ['user_id'], { + name: 'idx_collab_participants_user_id', + }); + + await queryInterface.addIndex('collaboration_participants', ['session_id', 'user_id'], { + name: 'idx_collab_participants_session_user', + unique: false, // Allow multiple entries for rejoining + }); + + await queryInterface.addIndex('collaboration_participants', ['is_active'], { + name: 'idx_collab_participants_is_active', + }); +} + +export async function down(queryInterface: QueryInterface): Promise { + // Remove indexes + await queryInterface.removeIndex('collaboration_participants', 'idx_collab_participants_is_active'); + await queryInterface.removeIndex('collaboration_participants', 'idx_collab_participants_session_user'); + await queryInterface.removeIndex('collaboration_participants', 'idx_collab_participants_user_id'); + await queryInterface.removeIndex('collaboration_participants', 'idx_collab_participants_session_id'); + await queryInterface.removeIndex('collaboration_sessions', 'idx_collab_sessions_status'); + await queryInterface.removeIndex('collaboration_sessions', 'idx_collab_sessions_lab_id'); + await queryInterface.removeIndex('collaboration_sessions', 'idx_collab_sessions_host_id'); + + // Drop tables + await queryInterface.dropTable('collaboration_participants'); + await queryInterface.dropTable('collaboration_sessions'); +} diff --git a/backend/src/models/CollaborationParticipant.ts b/backend/src/models/CollaborationParticipant.ts new file mode 100644 index 0000000..c023b18 --- /dev/null +++ b/backend/src/models/CollaborationParticipant.ts @@ -0,0 +1,126 @@ +import { Model, DataTypes, Optional } from 'sequelize'; +import { db } from '../database'; +import { User } from './User'; +import { CollaborationSession } from './CollaborationSession'; + +/** + * CollaborationParticipant attributes + */ +interface CollaborationParticipantAttributes { + id: string; + sessionId: string; + userId: string; + role: 'host' | 'participant' | 'viewer'; + joinedAt: Date; + leftAt?: Date; + isActive: boolean; + createdAt?: Date; + updatedAt?: Date; +} + +/** + * Attributes required for creating a collaboration participant + */ +interface CollaborationParticipantCreationAttributes + extends Optional {} + +/** + * CollaborationParticipant Model + * Tracks users participating in collaboration sessions + */ +class CollaborationParticipant + extends Model + implements CollaborationParticipantAttributes +{ + public id!: string; + public sessionId!: string; + public userId!: string; + public role!: 'host' | 'participant' | 'viewer'; + public joinedAt!: Date; + public leftAt!: Date; + public isActive!: boolean; + public readonly createdAt!: Date; + public readonly updatedAt!: Date; + + // Associations + public readonly session?: CollaborationSession; + public readonly user?: User; +} + +CollaborationParticipant.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + sessionId: { + type: DataTypes.UUID, + allowNull: false, + field: 'session_id', + }, + userId: { + type: DataTypes.UUID, + allowNull: false, + field: 'user_id', + }, + role: { + type: DataTypes.ENUM('host', 'participant', 'viewer'), + defaultValue: 'participant', + allowNull: false, + }, + joinedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'joined_at', + }, + leftAt: { + type: DataTypes.DATE, + allowNull: true, + field: 'left_at', + }, + isActive: { + type: DataTypes.BOOLEAN, + defaultValue: true, + allowNull: false, + field: 'is_active', + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'created_at', + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'updated_at', + }, + }, + { + sequelize: db, + tableName: 'collaboration_participants', + timestamps: true, + underscored: true, + } +); + +// Define associations +CollaborationParticipant.belongsTo(CollaborationSession, { + foreignKey: 'sessionId', + as: 'session', +}); + +CollaborationParticipant.belongsTo(User, { + foreignKey: 'userId', + as: 'user', +}); + +CollaborationSession.hasMany(CollaborationParticipant, { + foreignKey: 'sessionId', + as: 'participants', +}); + +export { CollaborationParticipant, CollaborationParticipantAttributes, CollaborationParticipantCreationAttributes }; diff --git a/backend/src/models/CollaborationSession.ts b/backend/src/models/CollaborationSession.ts new file mode 100644 index 0000000..737f8e6 --- /dev/null +++ b/backend/src/models/CollaborationSession.ts @@ -0,0 +1,150 @@ +import { Model, DataTypes, Optional } from 'sequelize'; +import { db } from '../database'; +import { User } from './User'; +import { Lab } from './Lab'; + +/** + * CollaborationSession attributes + */ +interface CollaborationSessionAttributes { + id: string; + name: string; + hostId: string; + labId?: string; + status: 'active' | 'ended' | 'scheduled'; + screenSharingUserId?: string; + screenSharingStreamId?: string; + maxParticipants: number; + startedAt: Date; + endedAt?: Date; + createdAt?: Date; + updatedAt?: Date; +} + +/** + * Attributes required for creating a collaboration session + */ +interface CollaborationSessionCreationAttributes + extends Optional {} + +/** + * CollaborationSession Model + * Manages real-time collaboration sessions between users + */ +class CollaborationSession + extends Model + implements CollaborationSessionAttributes +{ + public id!: string; + public name!: string; + public hostId!: string; + public labId!: string; + public status!: 'active' | 'ended' | 'scheduled'; + public screenSharingUserId!: string; + public screenSharingStreamId!: string; + public maxParticipants!: number; + public startedAt!: Date; + public endedAt!: Date; + public readonly createdAt!: Date; + public readonly updatedAt!: Date; + + // Associations + public readonly host?: User; + public readonly lab?: Lab; + public readonly participants?: any[]; +} + +CollaborationSession.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + name: { + type: DataTypes.STRING(255), + allowNull: false, + validate: { + notEmpty: true, + len: [1, 255], + }, + }, + hostId: { + type: DataTypes.UUID, + allowNull: false, + field: 'host_id', + }, + labId: { + type: DataTypes.UUID, + allowNull: true, + field: 'lab_id', + }, + status: { + type: DataTypes.ENUM('active', 'ended', 'scheduled'), + defaultValue: 'active', + allowNull: false, + }, + screenSharingUserId: { + type: DataTypes.UUID, + allowNull: true, + field: 'screen_sharing_user_id', + }, + screenSharingStreamId: { + type: DataTypes.STRING(255), + allowNull: true, + field: 'screen_sharing_stream_id', + }, + maxParticipants: { + type: DataTypes.INTEGER, + defaultValue: 10, + allowNull: false, + field: 'max_participants', + validate: { + min: 2, + max: 50, + }, + }, + startedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'started_at', + }, + endedAt: { + type: DataTypes.DATE, + allowNull: true, + field: 'ended_at', + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'created_at', + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + field: 'updated_at', + }, + }, + { + sequelize: db, + tableName: 'collaboration_sessions', + timestamps: true, + underscored: true, + } +); + +// Define associations +CollaborationSession.belongsTo(User, { + foreignKey: 'hostId', + as: 'host', +}); + +CollaborationSession.belongsTo(Lab, { + foreignKey: 'labId', + as: 'lab', +}); + +export { CollaborationSession, CollaborationSessionAttributes, CollaborationSessionCreationAttributes }; diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 8979e27..497275f 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -7,3 +7,5 @@ export { User, UserRole } from './User'; export { Lab, LabCategory, LabDifficulty, ContainerConfig } from './Lab'; export { LabInstance, LabInstanceStatus } from './LabInstance'; export { UserProgress, ProgressStatus } from './UserProgress'; +export { CollaborationSession, CollaborationSessionAttributes, CollaborationSessionCreationAttributes } from './CollaborationSession'; +export { CollaborationParticipant, CollaborationParticipantAttributes, CollaborationParticipantCreationAttributes } from './CollaborationParticipant'; diff --git a/backend/src/routes/collaboration.routes.ts b/backend/src/routes/collaboration.routes.ts index e97e85b..3f8dc63 100644 --- a/backend/src/routes/collaboration.routes.ts +++ b/backend/src/routes/collaboration.routes.ts @@ -1,59 +1,67 @@ import { Router } from 'express'; +import Joi from 'joi'; import { authenticate } from '@middleware/auth'; +import { validate } from '@middleware/validate'; +import { CollaborationController } from '@controllers/CollaborationController'; const router = Router(); +/** + * Validation schemas + */ +const createSessionSchema = { + body: Joi.object({ + name: Joi.string().min(1).max(255).required(), + labId: Joi.string().uuid().optional().allow(null), + maxParticipants: Joi.number().min(2).max(50).optional().default(10), + }), +}; + /** * GET /api/collaboration/sessions * Get all collaboration sessions for the authenticated user - * TODO: Implement full collaboration functionality */ -router.get('/sessions', authenticate, (_req, res) => { - // Stub implementation - return empty array for now - res.json({ - success: true, - data: [], - message: 'Collaboration feature coming soon', - }); -}); +router.get('/sessions', authenticate, CollaborationController.getSessions); + +/** + * GET /api/collaboration/sessions/active + * Get all active collaboration sessions (public) + */ +router.get('/sessions/active', authenticate, CollaborationController.getActiveSessions); + +/** + * GET /api/collaboration/sessions/:sessionId + * Get a specific collaboration session + */ +router.get('/sessions/:sessionId', authenticate, CollaborationController.getSession); /** * POST /api/collaboration/create * Create a new collaboration session - * TODO: Implement full collaboration functionality */ -router.post('/create', authenticate, (_req, res) => { - // Stub implementation - res.status(501).json({ - success: false, - message: 'Collaboration feature is not yet implemented', - }); -}); +router.post( + '/create', + authenticate, + validate(createSessionSchema), + CollaborationController.createSession +); /** * POST /api/collaboration/:sessionId/join * Join an existing collaboration session - * TODO: Implement full collaboration functionality */ -router.post('/:sessionId/join', authenticate, (_req, res) => { - // Stub implementation - res.status(501).json({ - success: false, - message: 'Collaboration feature is not yet implemented', - }); -}); +router.post('/:sessionId/join', authenticate, CollaborationController.joinSession); /** * POST /api/collaboration/:sessionId/leave * Leave a collaboration session - * TODO: Implement full collaboration functionality - */ -router.post('/:sessionId/leave', authenticate, (_req, res) => { - // Stub implementation - res.status(501).json({ - success: false, - message: 'Collaboration feature is not yet implemented', - }); -}); + */ +router.post('/:sessionId/leave', authenticate, CollaborationController.leaveSession); + +/** + * DELETE /api/collaboration/:sessionId + * End/delete a collaboration session (host only) + */ +router.delete('/:sessionId', authenticate, CollaborationController.endSession); export default router;