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 0047385..3b1e870 100644 --- a/apps/backend/src/users/entities/user.entity.ts +++ b/apps/backend/src/users/entities/user.entity.ts @@ -9,6 +9,8 @@ import { export enum UserRole { USER = 'user', 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;