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
15 changes: 11 additions & 4 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@ import { JwtStrategy } from './strategies/jwt.strategy';
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET') || 'your-secret-key',
useFactory: async (
configService: ConfigService,
): Promise<{
secret: string;
signOptions: { expiresIn: string };
}> => ({
secret:
configService.get<string>('JWT_SECRET') ?? 'your-secret-key',
signOptions: {
expiresIn: parseInt(configService.get<string>('JWT_EXPIRES_IN') || '900', 10), // 900s = 15m
expiresIn:
configService.get<string>('JWT_EXPIRES_IN') ?? '15m',
},
}),
}),
Expand All @@ -27,4 +34,4 @@ import { JwtStrategy } from './strategies/jwt.strategy';
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
export class AuthModule {}
18 changes: 18 additions & 0 deletions src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
});
}

async validate(payload: any) {
return { sub: payload.sub, email: payload.email };
}
}
27 changes: 27 additions & 0 deletions src/courses/guards/ws-jwt-auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { Socket } from "socket.io";

@Injectable()
export class WsJwtAuthGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const client: Socket = context.switchToWs().getClient<Socket>();
const token = client.handshake.auth?.token || client.handshake.headers?.authorization?.split(" ")[1];

if (!token) {
client.disconnect(true);
return false;
}

try {
const payload = await this.jwtService.verifyAsync(token);
(client as any).user = payload; // attach user context
return true;
} catch (err) {
client.disconnect(true);
return false;
}
}
}
25 changes: 25 additions & 0 deletions src/gateways/messaging.gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
ConnectedSocket,
OnGatewayConnection,
UseGuards,
} from "@nestjs/websockets";
import { Socket } from "socket.io";
import { WsJwtAuthGuard } from "../common/guards/ws-jwt-auth.guard";

@WebSocketGateway({ namespace: "/messaging" })
export class MessagingGateway implements OnGatewayConnection {
async handleConnection(client: Socket) {
// Guard will disconnect unauthorized clients
}

@UseGuards(WsJwtAuthGuard)
@SubscribeMessage("send_message")
async handleMessage(@MessageBody() data: any, @ConnectedSocket() client: Socket) {
const user = (client as any).user;
// process message with authenticated user context
return { userId: user.sub, message: data };
}
}
19 changes: 19 additions & 0 deletions src/gateways/notifications.gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
WebSocketGateway,
SubscribeMessage,
ConnectedSocket,
UseGuards,
} from "@nestjs/websockets";
import { Socket } from "socket.io";
import { WsJwtAuthGuard } from "../common/guards/ws-jwt-auth.guard";

@WebSocketGateway({ namespace: "/notifications" })
export class NotificationsGateway {
@UseGuards(WsJwtAuthGuard)
@SubscribeMessage("subscribe_notifications")
async handleSubscribe(@ConnectedSocket() client: Socket) {
const user = (client as any).user;
// subscribe user to notifications
return { userId: user.sub, subscribed: true };
}
}
21 changes: 21 additions & 0 deletions src/moderation/analytics/moderation-analytics.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ModerationEvent } from './moderation-event.entity';

@Injectable()
export class ModerationAnalyticsService {
constructor(
@InjectRepository(ModerationEvent)
private readonly eventRepo: Repository<ModerationEvent>,
) {}

async logModerationEvent(content: string, score: number, status: string) {
const event = this.eventRepo.create({ content, score, status });
await this.eventRepo.save(event);
}

async getAnalytics() {
return this.eventRepo.find({ order: { timestamp: 'DESC' } });
}
}
19 changes: 19 additions & 0 deletions src/moderation/analytics/moderation-event.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';

@Entity()
export class ModerationEvent {
@PrimaryGeneratedColumn()
id: number;

@Column('text')
content: string;

@Column('float')
score: number;

@Column()
status: string;

@CreateDateColumn()
timestamp: Date;
}
28 changes: 28 additions & 0 deletions src/moderation/auto/auto-moderation.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { HfInference } from '@huggingface/inference';

@Injectable()
export class AutoModerationService {
private hf: HfInference;

constructor() {
this.hf = new HfInference(process.env.HUGGINGFACE_API_KEY);
}

async analyze(content: string): Promise<{ flagged: boolean; reasons: string[]; score: number }> {
const result = await this.hf.textClassification({
model: 's-nlp/roberta_toxicity_classifier', // or 'unitary/toxic-bert'
inputs: content,
});

// result is an array of { label, score }
const toxicLabel = result.find(r => r.label.toLowerCase().includes('toxic'));
const score = toxicLabel ? toxicLabel.score : 0;

return {
flagged: score > 0.7, // threshold can be tuned
reasons: score > 0.7 ? ['AI model detected toxicity'] : [],
score,
};
}
}
28 changes: 28 additions & 0 deletions src/moderation/manual/manual-review.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ReviewItem } from './review-item.entity';

@Injectable()
export class ManualReviewService {
constructor(
@InjectRepository(ReviewItem)
private readonly reviewRepo: Repository<ReviewItem>,
) {}

async enqueue(content: string, safetyScore: number) {
const item = this.reviewRepo.create({ content, safetyScore, status: 'pending' });
await this.reviewRepo.save(item);
}

async getQueue(): Promise<ReviewItem[]> {
return this.reviewRepo.find({
where: { status: 'pending' },
order: { safetyScore: 'DESC', createdAt: 'ASC' }, // prioritize high risk, then oldest
});
}

async markReviewed(id: number) {
await this.reviewRepo.update(id, { status: 'reviewed' });
}
}
19 changes: 19 additions & 0 deletions src/moderation/manual/review-item.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';

@Entity()
export class ReviewItem {
@PrimaryGeneratedColumn()
id: number;

@Column('text')
content: string;

@Column('float')
safetyScore: number;

@Column({ default: 'pending' })
status: 'pending' | 'reviewed';

@CreateDateColumn()
createdAt: Date;
}
22 changes: 22 additions & 0 deletions src/moderation/moderation.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ModerationService } from './moderation.service';
import { AutoModerationService } from './auto/auto-moderation.service';
import { ManualReviewService } from './manual/manual-review.service';
import { ContentSafetyService } from './safety/content-safety.service';
import { ModerationAnalyticsService } from './analytics/moderation-analytics.service';
import { ReviewItem } from './manual/review-item.entity';
import { ModerationEvent } from './analytics/moderation-event.entity';

@Module({
imports: [TypeOrmModule.forFeature([ReviewItem, ModerationEvent])],
providers: [
ModerationService,
AutoModerationService,
ManualReviewService,
ContentSafetyService,
ModerationAnalyticsService,
],
exports: [ModerationService],
})
export class ModerationModule {}
29 changes: 29 additions & 0 deletions src/moderation/moderation.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common';
import { AutoModerationService } from './auto/auto-moderation.service';
import { ManualReviewService } from './manual/manual-review.service';
import { ContentSafetyService } from './safety/content-safety.service';
import { ModerationAnalyticsService } from './analytics/moderation-analytics.service';

@Injectable()
export class ModerationService {
constructor(
private readonly autoModeration: AutoModerationService,
private readonly manualReview: ManualReviewService,
private readonly safetyService: ContentSafetyService,
private readonly analytics: ModerationAnalyticsService,
) {}

async moderateContent(content: string) {
const autoResult = await this.autoModeration.analyze(content);
const safetyScore = this.safetyService.scoreContent(content);

if (autoResult.flagged || safetyScore > 0.7) {
await this.manualReview.enqueue(content, safetyScore);
this.analytics.logModerationEvent(content, safetyScore, 'flagged');
return { status: 'flagged', safetyScore };
}

this.analytics.logModerationEvent(content, safetyScore, 'approved');
return { status: 'approved', safetyScore };
}
}
12 changes: 12 additions & 0 deletions src/moderation/safety/content-safety.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class ContentSafetyService {
scoreContent(content: string): number {
// Simple scoring logic (replace with ML model later)
let score = 0;
if (/violence|hate|explicit/i.test(content)) score += 0.8;
if (/spam|scam/i.test(content)) score += 0.5;
return Math.min(score, 1);
}
}
Loading