Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "TeamUp Backend - Real-time collaboration platform",
"main": "dist/server.js",
"scripts": {
"dev": "nodemon src/server.ts",
"dev": "nodemon -w src -e ts --exec \"ts-node -r tsconfig-paths/register src/server.ts\"",
"build": "tsc",
"start": "node dist/server.js"
},
Expand Down Expand Up @@ -50,6 +50,7 @@
"@types/passport-google-oauth20": "^2.0.17",
"nodemon": "^3.0.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3"
}
}
}
4 changes: 4 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import createPermissionRoutes from './modules/access-control/routes/permissionRo
import createChatRoutes from './modules/chat/routes/chatRoutes';
import createActivityRoutes from './modules/audit/routes/activityRoutes';
import createInviteRoutes from './modules/invitation/routes/inviteRoutes';
import { setupAutomationRoutes } from './modules/automation/routes';


import { errorHandler, injectContainer } from '@middlewares';
import { configurePassport } from './config/passport';
Expand Down Expand Up @@ -67,6 +69,8 @@ export const createApp = (container: Container) => {
app.use(`${API_PREFIX}${MODULE_BASE.CHAT}`, createChatRoutes(container));
app.use(`${API_PREFIX}${MODULE_BASE.INVITES}`, createInviteRoutes(container));
app.use(`${API_PREFIX}${MODULE_BASE.ACTIVITY}`, createActivityRoutes(container));
app.use(`${API_PREFIX}${MODULE_BASE.AUTOMATION}`, setupAutomationRoutes(container.automationCtrl));


app.use(errorHandler);

Expand Down
21 changes: 21 additions & 0 deletions backend/src/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ import { TeamController } from '../modules/team/controllers/TeamController';
import { WorkshopController } from '../modules/workshop/controllers/WorkshopController';
import { WorkshopProjectController } from '../modules/project/controllers/WorkshopProjectController';
import { WorkshopTaskController } from '../modules/task/controllers/WorkshopTaskController';
import { AutomationService } from '../modules/automation/services/AutomationService';
import { AutomationController } from '../modules/automation/controllers/AutomationController';
import { IAutomationService } from '../modules/automation/interfaces/IAutomationService';


import { ITokenProvider } from '../shared/interfaces/ITokenProvider';
import { IEmailProvider } from '../shared/interfaces/IEmailProvider';
Expand Down Expand Up @@ -111,6 +115,8 @@ export class DIContainer implements Container {
public workshopProjectSrv: IWorkshopProjectService;
public workshopSrv: IWorkshopService;
public workshopTaskSrv: IWorkshopTaskService;
public automationSrv: IAutomationService;


public activityCtrl: ActivityController;
public auditCtrl: AuditController;
Expand All @@ -124,6 +130,8 @@ export class DIContainer implements Container {
public workshopCtrl: WorkshopController;
public workshopProjectCtrl: WorkshopProjectController;
public workshopTaskCtrl: WorkshopTaskController;
public automationCtrl: AutomationController;


constructor(httpServer: HTTPServer) {
this.tokenProv = new TokenProvider();
Expand Down Expand Up @@ -226,6 +234,13 @@ export class DIContainer implements Container {
this.socketSrv
);

this.automationSrv = new AutomationService(
this.workshopTaskRepo,
this.notificationRepo,
this.auditSrv
);


this.invitationSrv = new InvitationService(
this.invitationRepo,
this.workshopSrv,
Expand Down Expand Up @@ -267,5 +282,11 @@ export class DIContainer implements Container {

this.workshopTaskCtrl = new WorkshopTaskController(this.workshopTaskSrv);
this.workshopTaskCtrl.setSocketService(this.socketSrv);

this.automationCtrl = new AutomationController(
this.automationSrv,
this.workshopRepo
);

}
}
7 changes: 7 additions & 0 deletions backend/src/di/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ import { WorkshopProjectController } from '../modules/project/controllers/Worksh
import { WorkshopTaskController } from '../modules/task/controllers/WorkshopTaskController';
import { IInvitationService } from '../modules/invitation/interfaces/IInvitationService';
import { IInvitationRepository } from '../modules/invitation/interfaces/IInvitationRepository';
import { IAutomationService } from '../modules/automation/interfaces/IAutomationService';
import { AutomationController } from '../modules/automation/controllers/AutomationController';


export interface Container {
tokenProv: ITokenProvider;
Expand Down Expand Up @@ -77,6 +80,8 @@ export interface Container {
workshopProjectSrv: IWorkshopProjectService;
workshopSrv: IWorkshopService;
workshopTaskSrv: IWorkshopTaskService;
automationSrv: IAutomationService;


activityCtrl: ActivityController;
auditCtrl: AuditController;
Expand All @@ -90,4 +95,6 @@ export interface Container {
workshopCtrl: WorkshopController;
workshopProjectCtrl: WorkshopProjectController;
workshopTaskCtrl: WorkshopTaskController;
automationCtrl: AutomationController;

}
8 changes: 7 additions & 1 deletion backend/src/modules/audit/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,15 @@ export enum AuditAction {
JOIN_REQUEST_REJECTED = 'join_request_rejected',
UNAUTHORIZED_ACCESS = 'unauthorized_access',
PROJECT_MANAGER_ASSIGNED = 'project_manager_assigned',
PROJECT_MAINTAINER_ASSIGNED = 'project_maintainer_assigned'
PROJECT_MAINTAINER_ASSIGNED = 'project_maintainer_assigned',

AUTOMATION_RULE_CREATED = 'automation_rule_created',
AUTOMATION_RULE_UPDATED = 'automation_rule_updated',
AUTOMATION_RULE_DELETED = 'automation_rule_deleted',
AUTOMATION_RULE_TRIGGERED = 'automation_rule_triggered'
}


export interface IAuditLog extends Document {
workshop: Types.ObjectId;
action: AuditAction;
Expand Down
69 changes: 69 additions & 0 deletions backend/src/modules/automation/controllers/AutomationController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Request, Response } from 'express';
import { IAutomationService } from '../interfaces/IAutomationService';
import { IWorkshopRepository } from '../../workshop/interfaces/IWorkshopRepository';
import { AuthorizationError } from '../../../shared/utils/errors';

export class AutomationController {
constructor(
private automationService: IAutomationService,
private workshopRepo: IWorkshopRepository
) { }

async createRule(req: Request, res: Response) {
const { workshopId } = req.params;
const userId = (req as any).user.id;

if (!(await this.workshopRepo.isOwnerOrManager(workshopId, userId))) {
throw new AuthorizationError('Only workshop admins can manage automation rules');
}

const rule = await this.automationService.createRule(workshopId, userId, req.body);
return res.status(201).json(rule);
}

async getRules(req: Request, res: Response) {
const { workshopId } = req.params;

// Any member can likely view rules, but you might want to restrict this

const rules = await this.automationService.getRules(workshopId);
return res.json(rules);
}

async updateRule(req: Request, res: Response) {
const { workshopId, ruleId } = req.params;
const userId = (req as any).user.id;

if (!(await this.workshopRepo.isOwnerOrManager(workshopId, userId))) {
throw new AuthorizationError('Only workshop admins can manage automation rules');
}

const rule = await this.automationService.updateRule(ruleId, userId, req.body);
return res.json(rule);
}

async deleteRule(req: Request, res: Response) {
const { workshopId, ruleId } = req.params;
const userId = (req as any).user.id;

if (!(await this.workshopRepo.isOwnerOrManager(workshopId, userId))) {
throw new AuthorizationError('Only workshop admins can manage automation rules');
}

await this.automationService.deleteRule(ruleId, userId);
return res.status(204).send();
}

async toggleRule(req: Request, res: Response) {
const { workshopId, ruleId } = req.params;
const userId = (req as any).user.id;
const { isActive } = req.body;

if (!(await this.workshopRepo.isOwnerOrManager(workshopId, userId))) {
throw new AuthorizationError('Only workshop admins can manage automation rules');
}

const rule = await this.automationService.toggleRule(ruleId, userId, isActive);
return res.json(rule);
}
}
12 changes: 12 additions & 0 deletions backend/src/modules/automation/interfaces/IAutomationService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IAutomationRule, CreateAutomationRuleDTO } from '../types/index';

export interface IAutomationService {
createRule(workshopId: string, userId: string, data: CreateAutomationRuleDTO): Promise<IAutomationRule>;
getRules(workshopId: string): Promise<IAutomationRule[]>;
updateRule(ruleId: string, userId: string, updates: Partial<CreateAutomationRuleDTO>): Promise<IAutomationRule>;
deleteRule(ruleId: string, userId: string): Promise<void>;
toggleRule(ruleId: string, userId: string, isActive: boolean): Promise<IAutomationRule>;

// Execution core
handleEvent(triggerType: string, context: any): Promise<void>;
}
73 changes: 73 additions & 0 deletions backend/src/modules/automation/models/AutomationRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import mongoose, { Schema, Document } from 'mongoose';
import { IAutomationRule, AutomationTriggerType, AutomationActionType } from '../types/index';

const automationRuleSchema = new Schema<IAutomationRule & Document>(
{
workshopId: {
type: Schema.Types.ObjectId,
ref: 'Workshop',
required: true,
index: true
},
name: {
type: String,
required: true,
trim: true
},
description: {
type: String,
trim: true
},
trigger: {
type: {
type: String,
enum: Object.values(AutomationTriggerType),
required: true
},
config: {
type: Map,
of: Schema.Types.Mixed
}
},
conditions: [
{
field: { type: String, required: true },
operator: {
type: String,
enum: ['equals', 'not_equals', 'contains', 'greater_than', 'less_than'],
required: true
},
value: { type: Schema.Types.Mixed, required: true }
}
],
actions: [
{
type: {
type: String,
enum: Object.values(AutomationActionType),
required: true
},
config: {
type: Map,
of: Schema.Types.Mixed
}
}
],
isActive: {
type: Boolean,
default: true
},
createdBy: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
}
},
{
timestamps: true
}
);

automationRuleSchema.index({ workshopId: 1, isActive: 1 });

export const AutomationRule = mongoose.model<IAutomationRule & Document>('AutomationRule', automationRuleSchema);
18 changes: 18 additions & 0 deletions backend/src/modules/automation/routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Router } from 'express';
import { AutomationController } from '../controllers/AutomationController';
import { authenticate } from '../../../shared/middlewares/auth';
import { asyncHandler } from '../../../shared/middlewares/errorMiddleware';

export function setupAutomationRoutes(controller: AutomationController): Router {
const router = Router();

router.use(authenticate);

router.post('/:workshopId/rules', asyncHandler((req, res) => controller.createRule(req, res)));
router.get('/:workshopId/rules', asyncHandler((req, res) => controller.getRules(req, res)));
router.put('/:workshopId/rules/:ruleId', asyncHandler((req, res) => controller.updateRule(req, res)));
router.delete('/:workshopId/rules/:ruleId', asyncHandler((req, res) => controller.deleteRule(req, res)));
router.patch('/:workshopId/rules/:ruleId/toggle', asyncHandler((req, res) => controller.toggleRule(req, res)));

return router;
}
Loading