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
46 changes: 46 additions & 0 deletions apps/backend/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
@@ -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);
}


}
22 changes: 22 additions & 0 deletions apps/backend/src/admin/admin.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
27 changes: 27 additions & 0 deletions apps/backend/src/admin/audit-log/audit-log.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
17 changes: 17 additions & 0 deletions apps/backend/src/admin/audit-log/audit-log.service.ts
Original file line number Diff line number Diff line change
@@ -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<AuditLog>,
) {}

public async logAction(data: Partial<AuditLog>) {
const log = this.auditRepo.create(data);
return this.auditRepo.save(log);
}
}
15 changes: 15 additions & 0 deletions apps/backend/src/admin/http/test.endpoint.http
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions apps/backend/src/admin/providers/admin.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class AdminService {

}
61 changes: 61 additions & 0 deletions apps/backend/src/admin/providers/moderation.service.ts
Original file line number Diff line number Diff line change
@@ -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<Post>,

@InjectRepository(User)
private readonly userRepo: Repository<User>,

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.' };
}


}
4 changes: 4 additions & 0 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -55,6 +57,8 @@ console.log('Current environment:', ENV);
SignalGatewayModule,
MailModule,
NewsModule,
ForumReportModule,
AdminModule,
],
controllers: [AppController, RedisController],
providers: [AppService],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class CreateForumReportDto {}
4 changes: 4 additions & 0 deletions apps/backend/src/forum-report/dto/update-forum-report.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateForumReportDto } from './create-forum-report.dto';

export class UpdateForumReportDto extends PartialType(CreateForumReportDto) {}
26 changes: 26 additions & 0 deletions apps/backend/src/forum-report/entities/forum-report.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
20 changes: 20 additions & 0 deletions apps/backend/src/forum-report/forum-report.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -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>(ForumReportController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
9 changes: 9 additions & 0 deletions apps/backend/src/forum-report/forum-report.controller.ts
Original file line number Diff line number Diff line change
@@ -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
){}
}
9 changes: 9 additions & 0 deletions apps/backend/src/forum-report/forum-report.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
18 changes: 18 additions & 0 deletions apps/backend/src/forum-report/forum-report.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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>(ForumReportService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
19 changes: 19 additions & 0 deletions apps/backend/src/forum-report/forum-report.service.ts
Original file line number Diff line number Diff line change
@@ -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<ForumReport>,
) {}

public async getOpenReports() {
return this.reportRepo.find({
where: { resolved: false },
order: { createdAt: 'DESC' }
});
}
}
3 changes: 3 additions & 0 deletions apps/backend/src/stats/http/test.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Get basic system stats
GET http://localhost:3000/admin/stats?from=2024-01-01&to=2025-04-30
Authorization: Bearer
38 changes: 38 additions & 0 deletions apps/backend/src/stats/stats.service.ts
Original file line number Diff line number Diff line change
@@ -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<Post>,
@InjectRepository(ForumReport) private reportRepo: Repository<ForumReport>,
@InjectRepository(User) private userRepo: Repository<User>,
) {}

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,
};
}
}

6 changes: 6 additions & 0 deletions apps/backend/src/users/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
export enum UserRole {
USER = 'user',
ADMIN = 'admin',
Moderator = 'moderator',
Viewer = 'viewer',
}

@Entity()
Expand All @@ -25,6 +27,10 @@ export class User {
@Column({ default: false })
isVerified: boolean;

@Column({ default: false })
isShadowbanned: boolean;


@Column({ type: 'jsonb', nullable: true })
preferences: Record<string, any>;

Expand Down
Loading