From dacbca96a513ecb26d09da5c18f7973ee19afb5a Mon Sep 17 00:00:00 2001 From: Ahmad Abdulkareem Date: Wed, 30 Apr 2025 13:11:56 +0000 Subject: [PATCH] Developed Backend Endpoints for Multi-User Admin Tools and Moderation --- apps/backend/package-lock.json | 9 +-- apps/backend/src/admin/admin.controller.ts | 46 ++++++++++++++ apps/backend/src/admin/admin.module.ts | 22 +++++++ .../src/admin/audit-log/audit-log.entity.ts | 27 ++++++++ .../src/admin/audit-log/audit-log.service.ts | 17 ++++++ .../backend/src/admin/http/test.endpoint.http | 15 +++++ .../src/admin/providers/admin.service.ts | 6 ++ .../src/admin/providers/moderation.service.ts | 61 +++++++++++++++++++ apps/backend/src/app.module.ts | 4 ++ .../dto/create-forum-report.dto.ts | 1 + .../dto/update-forum-report.dto.ts | 4 ++ .../entities/forum-report.entity.ts | 26 ++++++++ .../forum-report.controller.spec.ts | 20 ++++++ .../forum-report/forum-report.controller.ts | 9 +++ .../src/forum-report/forum-report.module.ts | 9 +++ .../forum-report/forum-report.service.spec.ts | 18 ++++++ .../src/forum-report/forum-report.service.ts | 19 ++++++ apps/backend/src/stats/http/test.http | 3 + apps/backend/src/stats/stats.service.ts | 38 ++++++++++++ .../backend/src/users/entities/user.entity.ts | 8 ++- 20 files changed, 357 insertions(+), 5 deletions(-) create mode 100644 apps/backend/src/admin/admin.controller.ts create mode 100644 apps/backend/src/admin/admin.module.ts create mode 100644 apps/backend/src/admin/audit-log/audit-log.entity.ts create mode 100644 apps/backend/src/admin/audit-log/audit-log.service.ts create mode 100644 apps/backend/src/admin/http/test.endpoint.http create mode 100644 apps/backend/src/admin/providers/admin.service.ts create mode 100644 apps/backend/src/admin/providers/moderation.service.ts create mode 100644 apps/backend/src/forum-report/dto/create-forum-report.dto.ts create mode 100644 apps/backend/src/forum-report/dto/update-forum-report.dto.ts create mode 100644 apps/backend/src/forum-report/entities/forum-report.entity.ts create mode 100644 apps/backend/src/forum-report/forum-report.controller.spec.ts create mode 100644 apps/backend/src/forum-report/forum-report.controller.ts create mode 100644 apps/backend/src/forum-report/forum-report.module.ts create mode 100644 apps/backend/src/forum-report/forum-report.service.spec.ts create mode 100644 apps/backend/src/forum-report/forum-report.service.ts create mode 100644 apps/backend/src/stats/http/test.http create mode 100644 apps/backend/src/stats/stats.service.ts diff --git a/apps/backend/package-lock.json b/apps/backend/package-lock.json index ad65f51..cd897cf 100644 --- a/apps/backend/package-lock.json +++ b/apps/backend/package-lock.json @@ -14975,15 +14975,16 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.99.6", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.6.tgz", - "integrity": "sha512-TJOLrJ6oeccsGWPl7ujCYuc0pIq2cNsuD6GZDma8i5o5Npvcco/z+NKvZSFsP0/x6SShVb0+X2JK/JHUjKY9dQ==", + "version": "5.99.7", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.7.tgz", + "integrity": "sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", @@ -15000,7 +15001,7 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.0", + "schema-utils": "^4.3.2", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", diff --git a/apps/backend/src/admin/admin.controller.ts b/apps/backend/src/admin/admin.controller.ts new file mode 100644 index 0000000..219ef0b --- /dev/null +++ b/apps/backend/src/admin/admin.controller.ts @@ -0,0 +1,46 @@ +import { Controller, Get, Param, Patch, Post, Req, UseGuards } from "@nestjs/common"; +import { JwtAuthGuard } from "src/auth/guards/jwt-auth.guard"; +import { RolesGuard } from "src/auth/guards/roles.guard"; +import { Roles } from "src/auth/decorators/roles.decorator"; +import { UserRole } from "src/users/entities/user.entity"; +import { ForumReportService } from "src/forum-report/forum-report.service"; +import { ModerationService } from "./providers/moderation.service"; + + +@Controller('admin') +@UseGuards(JwtAuthGuard, RolesGuard ) + +export class AdminController { + constructor( + private readonly reportService: ForumReportService, + + private readonly moderationService: ModerationService + ) {} + + @Get('reports') + @Roles(UserRole.ADMIN, UserRole.Moderator) + public async getReports() { + return await this.reportService.getOpenReports(); + } + + @Get('stats') + @Roles(UserRole.ADMIN, UserRole.Moderator) + public async getStats(@Query() query) { + return await this.statsService.getSystemStats(query); +} + + + @Post('post/:id/ban') + @Roles(UserRole.ADMIN, UserRole.Moderator) + public async banPost(@Param('id') postId: string, @Req() req) { + return await this.moderationService.banPost(postId, req.user.wallet); +} + +@Patch('user/:wallet/shadowban') +@Roles(UserRole.ADMIN, UserRole.Moderator) +public async shadowbanUser(@Param('wallet') wallet: string, @Req() req) { + return await this.moderationService.shadowbanUser(wallet, req.user.wallet); +} + + +} diff --git a/apps/backend/src/admin/admin.module.ts b/apps/backend/src/admin/admin.module.ts new file mode 100644 index 0000000..8fd4f52 --- /dev/null +++ b/apps/backend/src/admin/admin.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { AdminService } from './providers/admin.service'; +import { AdminController } from './admin.controller'; +import { ForumReportService } from 'src/forum-report/forum-report.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ForumReport } from 'src/forum-report/entities/forum-report.entity'; +import { ModerationService } from './providers/moderation.service'; +import { AuditLogService } from './audit-log/audit-log.service'; +import { User } from 'src/users/entities/user.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Post, User, ForumReport])], + controllers: [AdminController], + providers: [ + AdminService, + ForumReportService, + ModerationService, + AuditLogService + ], + exports: [AdminService] +}) +export class AdminModule {} diff --git a/apps/backend/src/admin/audit-log/audit-log.entity.ts b/apps/backend/src/admin/audit-log/audit-log.entity.ts new file mode 100644 index 0000000..277d30d --- /dev/null +++ b/apps/backend/src/admin/audit-log/audit-log.entity.ts @@ -0,0 +1,27 @@ +import { + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn +} from "typeorm"; + +@Entity() +export class AuditLog { + @PrimaryGeneratedColumn() + id: number; + + @Column() + action: string; + + @Column() + performedBy: string; // wallet or user ID + + @Column() + targetId: string; + + @Column('text') + details: string; + + @CreateDateColumn() + timestamp: Date; +} diff --git a/apps/backend/src/admin/audit-log/audit-log.service.ts b/apps/backend/src/admin/audit-log/audit-log.service.ts new file mode 100644 index 0000000..5dac680 --- /dev/null +++ b/apps/backend/src/admin/audit-log/audit-log.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { AuditLog } from "./audit-log.entity"; + +@Injectable() +export class AuditLogService { + constructor( + @InjectRepository(AuditLog) + private readonly auditRepo: Repository, + ) {} + + public async logAction(data: Partial) { + const log = this.auditRepo.create(data); + return this.auditRepo.save(log); + } +} diff --git a/apps/backend/src/admin/http/test.endpoint.http b/apps/backend/src/admin/http/test.endpoint.http new file mode 100644 index 0000000..d4d355e --- /dev/null +++ b/apps/backend/src/admin/http/test.endpoint.http @@ -0,0 +1,15 @@ +### Ban a user admin/post/:id/ban + +POST http://localhost:3000/admin/post/1/ban +Authorization: Bearer +Content-Type: application/json +{ + +} + + +### Shadowban a user by wallet address admin/user/walletAddress/shadowban + +PATCH http://localhost:3000/admin/user/oxgkdkjjjjnsjh/shadowban +Authorization: Bearer +Content-Type: application/json diff --git a/apps/backend/src/admin/providers/admin.service.ts b/apps/backend/src/admin/providers/admin.service.ts new file mode 100644 index 0000000..3a70568 --- /dev/null +++ b/apps/backend/src/admin/providers/admin.service.ts @@ -0,0 +1,6 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AdminService { + +} diff --git a/apps/backend/src/admin/providers/moderation.service.ts b/apps/backend/src/admin/providers/moderation.service.ts new file mode 100644 index 0000000..794a45c --- /dev/null +++ b/apps/backend/src/admin/providers/moderation.service.ts @@ -0,0 +1,61 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; +import { AuditLogService } from "../audit-log/audit-log.service"; +import { User } from "src/users/entities/user.entity"; + +@Injectable() +export class ModerationService { + constructor( + // @InjectRepository() //needs post entity from lishman + // private readonly postRepo: Repository, + + @InjectRepository(User) + private readonly userRepo: Repository, + + private readonly auditService: AuditLogService, + + ) {} + + // public async banPost(postId: string, performedBy: string) { + // // const post = await this.postRepo.findOne({ where: { id: postId } }); + + // if (!post) { + // throw new NotFoundException('Post not found'); + // } + + // post.isBanned = true; + // await this.postRepo.save(post); + + // await this.auditService.logAction({ + // action: 'BAN_POST', + // performedBy, + // targetId: postId, + // details: `Post "${post.title}" was banned.`, + // }); + + // return { message: 'Post has been banned.' }; + // } + + public async shadowbanUser(walletAddress: string, performedBy: string) { + const user = await this.userRepo.findOne({ where: { walletAddress } }); + + if (!user) { + throw new NotFoundException('User not found'); + } + + user.isShadowbanned = true; + await this.userRepo.save(user); + + await this.auditService.logAction({ + action: 'SHADOWBAN_USER', + performedBy, + targetId: walletAddress, + details: `User with wallet ${walletAddress} was shadowbanned.`, + }); + + return { message: ' This User has been shadowbanned.' }; + } + + +} diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 1760162..959e6a4 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -18,6 +18,8 @@ import { RateLimitMiddleware } from './middleware/rate-limit.middleware'; import { MailModule } from './mail/mail.module'; import { NewsModule } from './news/news.module'; import { TasksModule } from './tasks/tasks.module'; +import { ForumReportModule } from './forum-report/forum-report.module'; +import { AdminModule } from './admin/admin.module'; const ENV = process.env.NODE_ENV || 'development'; console.log('Current environment:', ENV); @@ -55,6 +57,8 @@ console.log('Current environment:', ENV); SignalGatewayModule, MailModule, NewsModule, + ForumReportModule, + AdminModule, ], controllers: [AppController, RedisController], providers: [AppService], diff --git a/apps/backend/src/forum-report/dto/create-forum-report.dto.ts b/apps/backend/src/forum-report/dto/create-forum-report.dto.ts new file mode 100644 index 0000000..4606cc9 --- /dev/null +++ b/apps/backend/src/forum-report/dto/create-forum-report.dto.ts @@ -0,0 +1 @@ +export class CreateForumReportDto {} diff --git a/apps/backend/src/forum-report/dto/update-forum-report.dto.ts b/apps/backend/src/forum-report/dto/update-forum-report.dto.ts new file mode 100644 index 0000000..169a3f3 --- /dev/null +++ b/apps/backend/src/forum-report/dto/update-forum-report.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateForumReportDto } from './create-forum-report.dto'; + +export class UpdateForumReportDto extends PartialType(CreateForumReportDto) {} diff --git a/apps/backend/src/forum-report/entities/forum-report.entity.ts b/apps/backend/src/forum-report/entities/forum-report.entity.ts new file mode 100644 index 0000000..0710ea5 --- /dev/null +++ b/apps/backend/src/forum-report/entities/forum-report.entity.ts @@ -0,0 +1,26 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn +} from 'typeorm'; + +@Entity() +export class ForumReport { + @PrimaryGeneratedColumn() + id: number; + + @Column() + postId: string; + + @Column() + reason: string; + + @Column({ + default: false +}) + resolved: boolean; + + @CreateDateColumn() + createdAt: Date; +} diff --git a/apps/backend/src/forum-report/forum-report.controller.spec.ts b/apps/backend/src/forum-report/forum-report.controller.spec.ts new file mode 100644 index 0000000..b5f67de --- /dev/null +++ b/apps/backend/src/forum-report/forum-report.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ForumReportController } from './forum-report.controller'; +import { ForumReportService } from './forum-report.service'; + +describe('ForumReportController', () => { + let controller: ForumReportController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ForumReportController], + providers: [ForumReportService], + }).compile(); + + controller = module.get(ForumReportController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/backend/src/forum-report/forum-report.controller.ts b/apps/backend/src/forum-report/forum-report.controller.ts new file mode 100644 index 0000000..99c5cc5 --- /dev/null +++ b/apps/backend/src/forum-report/forum-report.controller.ts @@ -0,0 +1,9 @@ +import { Controller } from "@nestjs/common"; +import { ForumReportService } from "./forum-report.service"; + +@Controller('forum-report') +export class ForumReportController{ + constructor( + private readonly forumReportService:ForumReportService + ){} +} diff --git a/apps/backend/src/forum-report/forum-report.module.ts b/apps/backend/src/forum-report/forum-report.module.ts new file mode 100644 index 0000000..594289d --- /dev/null +++ b/apps/backend/src/forum-report/forum-report.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ForumReportService } from './forum-report.service'; +import { ForumReportController } from './forum-report.controller'; + +@Module({ + controllers: [ForumReportController], + providers: [ForumReportService], +}) +export class ForumReportModule {} diff --git a/apps/backend/src/forum-report/forum-report.service.spec.ts b/apps/backend/src/forum-report/forum-report.service.spec.ts new file mode 100644 index 0000000..7b2939e --- /dev/null +++ b/apps/backend/src/forum-report/forum-report.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ForumReportService } from './forum-report.service'; + +describe('ForumReportService', () => { + let service: ForumReportService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ForumReportService], + }).compile(); + + service = module.get(ForumReportService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/backend/src/forum-report/forum-report.service.ts b/apps/backend/src/forum-report/forum-report.service.ts new file mode 100644 index 0000000..87ad85b --- /dev/null +++ b/apps/backend/src/forum-report/forum-report.service.ts @@ -0,0 +1,19 @@ +import { InjectRepository } from "@nestjs/typeorm"; +import { ForumReport } from "./entities/forum-report.entity"; +import { Repository } from "typeorm"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class ForumReportService { + constructor( + @InjectRepository(ForumReport) //repo injection forum-report entity + private readonly reportRepo: Repository, + ) {} + + public async getOpenReports() { + return this.reportRepo.find({ + where: { resolved: false }, + order: { createdAt: 'DESC' } + }); + } +} diff --git a/apps/backend/src/stats/http/test.http b/apps/backend/src/stats/http/test.http new file mode 100644 index 0000000..38a71e6 --- /dev/null +++ b/apps/backend/src/stats/http/test.http @@ -0,0 +1,3 @@ +### Get basic system stats +GET http://localhost:3000/admin/stats?from=2024-01-01&to=2025-04-30 +Authorization: Bearer diff --git a/apps/backend/src/stats/stats.service.ts b/apps/backend/src/stats/stats.service.ts new file mode 100644 index 0000000..bc555ed --- /dev/null +++ b/apps/backend/src/stats/stats.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { ForumReport } from "src/forum-report/entities/forum-report.entity"; +import { User } from "src/users/entities/user.entity"; +import { Repository } from "typeorm"; + +@Injectable() +export class StatsService { + constructor( + @InjectRepository(Post) private postRepo: Repository, + @InjectRepository(ForumReport) private reportRepo: Repository, + @InjectRepository(User) private userRepo: Repository, + ) {} + + public async getSystemStats(query: { from?: string; to?: string }) { + const { from, to } = query; + const filter: any = {}; + + if (from || to) { + filter.createdAt = {}; + if (from) filter.createdAt['$gte'] = new Date(from); + if (to) filter.createdAt['$lte'] = new Date(to); + } + + const [totalPosts, totalReports, totalUsers] = await Promise.all([ + this.postRepo.count({ where: filter }), + this.reportRepo.count({ where: filter }), + this.userRepo.count(), + ]); + + return { + totalPosts, + totalReports, + totalUsers, + }; + } +} + diff --git a/apps/backend/src/users/entities/user.entity.ts b/apps/backend/src/users/entities/user.entity.ts index a6f9172..5d097cb 100644 --- a/apps/backend/src/users/entities/user.entity.ts +++ b/apps/backend/src/users/entities/user.entity.ts @@ -8,7 +8,9 @@ import { export enum UserRole { USER = 'user', - ADMIN = 'admin' + ADMIN = 'admin', + Moderator = 'moderator', + Viewer = 'viewer', } @Entity() @@ -25,6 +27,10 @@ export class User { @Column({ default: false }) isVerified: boolean; + @Column({ default: false }) + isShadowbanned: boolean; + + @Column({ type: 'jsonb', nullable: true }) preferences: Record;