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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
SLACK_WEBHOOK_URL=your_webhook_url_here
# Database Configuration
DATABASE_HOST=localhost
DATABASE_PORT=5432
Expand Down
149 changes: 22 additions & 127 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/analytics/analytics.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { AnalyticsService } from './analytics.service';
import { AnalyticsController } from './analytics.controller';
Expand All @@ -8,7 +8,7 @@ import { MetricsCollectionService } from '../monitoring/metrics/metrics-collecti
import { SegmentModule } from './segment/segment.module';

@Module({
imports: [FingerprintModule, SegmentModule],
imports: [forwardRef(() => FingerprintModule), SegmentModule],
providers: [
MetricsCollectionService,
AnalyticsService,
Expand Down
3 changes: 3 additions & 0 deletions src/analytics/fingerprint/fingerprint.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
ExecutionContext,
CallHandler,
Logger,
Inject,
forwardRef,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { Request } from 'express';
Expand All @@ -28,6 +30,7 @@ export class FingerprintInterceptor implements NestInterceptor {

constructor(
private readonly fingerprintService: FingerprintService,
@Inject(forwardRef(() => AnalyticsService))
private readonly analyticsService: AnalyticsService,
) {}

Expand Down
4 changes: 3 additions & 1 deletion src/analytics/fingerprint/fingerprint.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { FingerprintService } from './fingerprint.service';
import { FingerprintInterceptor } from './fingerprint.interceptor';
import { AnalyticsModule } from '../analytics.module';

@Module({
imports: [forwardRef(() => AnalyticsModule)],
providers: [FingerprintService, FingerprintInterceptor],
exports: [FingerprintService, FingerprintInterceptor],
})
Expand Down
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { HealthModule } from './health/health.module';
// ✅ keep BOTH modules
import { ReadReplicaModule } from './database/read-replica';
import { CachingModule } from './caching/caching.module';
import { SlackService } from './slack.service';
import { CoursesModule } from './courses/courses.module';

const featureFlags = loadFeatureFlags();
Expand Down Expand Up @@ -64,6 +65,7 @@ const featureFlags = loadFeatureFlags();
],
controllers: [AppController],
providers: [
SlackService,
...(featureFlags.ENABLE_RATE_LIMITING ? [{ provide: APP_GUARD, useClass: QuotaGuard }] : []),
{ provide: APP_INTERCEPTOR, useClass: RequestTimeoutInterceptor },
],
Expand Down
2 changes: 1 addition & 1 deletion src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { PermissionsGuard } from './guards/permissions.guard';
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: process.env.JWT_SECRET || 'default-jwt-secret',
signOptions: { expiresIn: process.env.JWT_EXPIRES_IN || '15m' },
signOptions: { expiresIn: (process.env.JWT_EXPIRES_IN || '15m') as any },
}),
TypeOrmModule.forFeature([User]),
],
Expand Down
6 changes: 3 additions & 3 deletions src/courses/courses.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class CoursesService {
async findAll(requestingUser?: User): Promise<Course[]> {
const isPrivileged =
requestingUser &&
requestingUser.roles.some(role => ['admin', 'moderator'].includes(role));
requestingUser.roles.some(role => ['admin', 'moderator'].includes(role.name));

if (isPrivileged) {
return this.courseRepo.find({ order: { createdAt: 'DESC' } });
Expand Down Expand Up @@ -223,14 +223,14 @@ export class CoursesService {
}

private assertPrivileged(user: User): void {
if (!user.roles.some(role => ['admin', 'moderator'].includes(role))) {
if (!user.roles.some(role => ['admin', 'moderator'].includes(role.name))) {
throw new ForbiddenException('Only admins or moderators may perform this action.');
}
}

private assertOwnerOrPrivileged(course: Course, user: User): void {
const isOwner = course.instructorId === user.id;
const isPrivileged = user.roles.some(role => ['admin', 'moderator'].includes(role));
const isPrivileged = user.roles.some(role => ['admin', 'moderator'].includes(role.name));
if (!isOwner && !isPrivileged) {
throw new ForbiddenException('Insufficient permissions.');
}
Expand Down
2 changes: 1 addition & 1 deletion src/email-marketing/services/email-tracking.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export class EmailTrackingService {
async updateReputation(score: number) {
// Implementation depends on where reputation is stored; placeholder for now
this.logger.log(`Reputation score updated to ${score}`);
}

async recordDelivered(data: Partial<EmailEvent>) {
const event = this.emailEventRepo.create({
Expand Down Expand Up @@ -78,5 +79,4 @@ export class EmailTrackingService {
this.logger.log(`Email click recorded for ${event.recipientId}`);
return event;
}
}
}
14 changes: 10 additions & 4 deletions src/incident-management/services/runbook-execution.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,16 @@ export class RunbookExecutionService {
let allSuccess = true;

for (const step of runbook.steps) {
const stepExecution = {
const stepExecution: {
stepNumber: number;
stepName: string;
status: 'in_progress' | 'completed' | 'failed';
output?: string;
error?: string;
} = {
stepNumber: step.stepNumber,
stepName: step.stepName,
status: 'in_progress' as const,
status: 'in_progress',
};

try {
Expand All @@ -84,12 +90,12 @@ export class RunbookExecutionService {
(stepExecution as any)['status'] = result.success ? 'completed' : 'failed';
stepExecution['output'] = result.output;
if (!result.success) {
stepExecution['error'] = result.error;
stepExecution.error = result.error;
allSuccess = false;
}

this.logger.log(
`Step ${step.stepNumber} completed: ${stepExecution['status']}`,
`Step ${step.stepNumber} completed: ${stepExecution.status}`,
);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
Expand Down
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { DatabaseShutdownService } from './database/services/database-shutdown.s
import { WorkerShutdownService } from './workers/services/worker-shutdown.service';
import { TIME, BYTES } from './common/constants/time.constants';
import { DecompressionMiddleware } from './common/middleware/decompression.middleware';
import { SlackService } from './slack.service';
import compression from 'compression';
import { FieldFilterInterceptor } from './common/interceptors/field-filter.interceptor';
import { ImageOptimizationInterceptor } from './common/interceptors/image-optimization.interceptor';
Expand Down Expand Up @@ -257,6 +258,8 @@ async function bootstrapWorker(): Promise<void> {

const port = process.env.PORT || 3000;
app.enableShutdownHooks();
const slackService = app.get(SlackService);
await slackService.sendAlert('TeachLink Backend has successfully started up on local system! 🚀', 'low');
await app.listen(port);

const startupTime = Date.now() - bootstrapStartTime;
Expand Down
4 changes: 2 additions & 2 deletions src/messaging/message.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Controller, Post, Body, Get, Param, Patch, UseGuards } from '@nestjs/common';
import { MessagingService } from './messaging.service';
import { CreateMessageDto, MarkReadDto } from './message.dto';
import { AuthGuard } from '../auth/auth.guard'; // Adjust path if auth guard location differs
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';

@UseGuards(AuthGuard)
@UseGuards(JwtAuthGuard)
@Controller('messages')
export class MessagingController {
constructor(private readonly messagingService: MessagingService) {}
Expand Down
6 changes: 3 additions & 3 deletions src/messaging/message.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
import { User } from '../users/user.entity'; // Assuming a User entity exists
import { User } from '../users/entities/user.entity'; // Assuming a User entity exists

@Entity({ name: 'messages' })
export class Message {
Expand All @@ -22,11 +22,11 @@ export class Message {
readAt: Date | null;

// Optional relations for convenience (may be lazy loaded)
@ManyToOne(() => User, (user) => user.sentMessages, { eager: false })
@ManyToOne(() => User, { eager: false })
@JoinColumn({ name: 'senderId' })
sender: User;

@ManyToOne(() => User, (user) => user.receivedMessages, { eager: false })
@ManyToOne(() => User, { eager: false })
@JoinColumn({ name: 'recipientId' })
recipient: User;
}
4 changes: 2 additions & 2 deletions src/messaging/messaging.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { BullModule } from '@nestjs/bull';
import { QUEUE_NAMES } from '../common/constants/queue.constants';
import { MessagingService } from './messaging.service';
import { MessageController } from './message.controller';
import { MessagingController } from './message.controller';
import { MessageGateway } from './message.gateway';
import { Message } from './message.entity';

Expand All @@ -13,7 +13,7 @@ import { Message } from './message.entity';
BullModule.registerQueue({ name: QUEUE_NAMES.MESSAGE_QUEUE }),
],
providers: [MessagingService, MessageGateway],
controllers: [MessageController],
controllers: [MessagingController],
exports: [MessagingService],
})
export class MessagingModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ export class CreateSchemaVersionAndChangeTables1680000000000 implements Migratio
name: 'schema_version',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, isGenerated: true, generationStrategy: 'uuid' },
{ name: 'schemaName', type: 'varchar', length: 255, isNullable: false },
{ name: 'schemaName', type: 'varchar', length: '255', isNullable: false },
{ name: 'definition', type: 'jsonb', isNullable: false },
{ name: 'checksum', type: 'varchar', length: 64, isNullable: false },
{ name: 'checksum', type: 'varchar', length: '64', isNullable: false },
{ name: 'createdAt', type: 'timestamptz', default: 'NOW()' },
{ name: 'updatedAt', type: 'timestamptz', default: 'NOW()' },
],
Expand All @@ -21,11 +21,11 @@ export class CreateSchemaVersionAndChangeTables1680000000000 implements Migratio
name: 'schema_change',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, isGenerated: true, generationStrategy: 'uuid' },
{ name: 'schemaName', type: 'varchar', length: 255, isNullable: false },
{ name: 'fromVersion', type: 'varchar', length: 50, isNullable: false },
{ name: 'toVersion', type: 'varchar', length: 50, isNullable: false },
{ name: 'schemaName', type: 'varchar', length: '255', isNullable: false },
{ name: 'fromVersion', type: 'varchar', length: '50', isNullable: false },
{ name: 'toVersion', type: 'varchar', length: '50', isNullable: false },
{ name: 'changeType', type: 'enum', enum: ['ADD_COLUMN','DROP_COLUMN','MODIFY_COLUMN','ADD_INDEX','DROP_INDEX','ADD_TABLE','DROP_TABLE','ADD_RELATION','DROP_RELATION'] },
{ name: 'fieldPath', type: 'varchar', length: 255, isNullable: false },
{ name: 'fieldPath', type: 'varchar', length: '255', isNullable: false },
{ name: 'previousValue', type: 'jsonb', isNullable: true },
{ name: 'newValue', type: 'jsonb', isNullable: true },
{ name: 'createdAt', type: 'timestamptz', default: 'NOW()' },
Expand Down
18 changes: 18 additions & 0 deletions src/modules/ccpa/dto/ccpa.controller.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import { Controller, UseGuards, Get, Patch, Delete } from '@nestjs/common';
import { JwtAuthGuard } from '../../../../auth/guards/jwt-auth.guard';

@Controller('ccpa')
@UseGuards(JwtAuthGuard)
export class CcpaController {
@Get('disclosure')
async getDisclosure(): Promise<any> {
return { status: 'success', disclosure: 'disclosure info' };
}

@Get('know')
async getKnow(): Promise<any> {
return { status: 'success', data: [] };
}

@Get('preferences')
async getPreferences(): Promise<any> {
return { status: 'success', preferences: {} };
}

@Patch('opt-out')
async patchOptOut(): Promise<any> {
return { status: 'success', optedOut: true };
}

@Delete('delete')
async deleteData(): Promise<any> {
return { status: 'success', deleted: true };
}
}
10 changes: 5 additions & 5 deletions src/rbac/roles/roles.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export class RolesService {
}

async findRoleById(id: string): Promise<Role> {
const role = await this.roleRepository.findOneBy({ id }, { relations: ['permissions'] });
const role = await this.roleRepository.findOne({ where: { id }, relations: ['permissions'] });
if (!role) {
throw new NotFoundException(`Role with ID ${id} not found`);
}
Expand All @@ -55,7 +55,7 @@ export class RolesService {
.set(permissions);
}

const updated = await this.roleRepository.findOneBy({ id }, { relations: ['permissions'] });
const updated = await this.roleRepository.findOne({ where: { id }, relations: ['permissions'] });
if (!updated) {
throw new NotFoundException(`Role with ID ${id} not found`);
}
Expand All @@ -70,12 +70,12 @@ export class RolesService {
}

async addPermissionToRole(roleId: string, permissionId: string): Promise<Role> {
const role = await this.roleRepository.findOneBy({ roleId }, { relations: ['permissions'] });
const role = await this.roleRepository.findOne({ where: { id: roleId }, relations: ['permissions'] });
if (!role) {
throw new NotFoundException(`Role with ID ${roleId} not found`);
}

const permission = await this.permissionRepository.findOneBy({ permissionId });
const permission = await this.permissionRepository.findOneBy({ id: permissionId });
if (!permission) {
throw new NotFoundException(`Permission with ID ${permissionId} not found`);
}
Expand All @@ -89,7 +89,7 @@ export class RolesService {
}

async removePermissionFromRole(roleId: string, permissionId: string): Promise<Role> {
const role = await this.roleRepository.findOneBy({ roleId }, { relations: ['permissions'] });
const role = await this.roleRepository.findOne({ where: { id: roleId }, relations: ['permissions'] });
if (!role) {
throw new NotFoundException(`Role with ID ${roleId} not found`);
}
Expand Down
36 changes: 36 additions & 0 deletions src/slack.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import axios from 'axios';

@Injectable()
export class SlackService {
// This automatically reads the secret URL you just pasted into your .env file
private readonly webhookUrl = process.env.SLACK_WEBHOOK_URL;

async sendAlert(message: string, severity: 'low' | 'medium' | 'high') {
if (!this.webhookUrl) {
console.error('Slack Webhook URL is missing in .env file!');
return;
}

// Choose an emoji based on how urgent the alert is
let emoji = 'ℹ️'; // Default for low
if (severity === 'high') {
emoji = '🚨';
} else if (severity === 'medium') {
emoji = '⚠️';
}

// Format the text nicely for your Slack channel
const payload = {
text: `${emoji} *TeachLink Alert* (${severity.toUpperCase()})\n${message}`,
};

try {
// Send the message over the internet to Slack
await axios.post(this.webhookUrl, payload);
console.log(`Slack alert (${severity}) sent successfully!`);
} catch (error) {
console.error('Failed to send Slack alert:', error.message);
}
}
}
2 changes: 1 addition & 1 deletion src/tenancy/tenancy.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
Query,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger';
import { ApiBearerAuth, ApiTags, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger';
import { TenancyService } from './tenancy.service';
import { TenantAdminService } from './admin/tenant-admin.service';
import { TenantBillingService } from './billing/tenant-billing.service';
Expand Down
Loading
Loading