Skip to content
Open
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
Binary file added backend/lint.txt
Binary file not shown.
23 changes: 23 additions & 0 deletions backend/lint2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

> [email protected] lint C:\Users\DELL\Desktop\Arena\InsightArena\backend
> eslint "{src,apps,libs,test}/**/*.ts" --fix


C:\Users\DELL\Desktop\Arena\InsightArena\backend\src\admin\admin.service.ts
35:24 error 'SystemConfigValues' is an 'error' type that acts as 'any' and overrides all other types in this union type @typescript-eslint/no-redundant-type-constituents
50:23 warning Unsafe argument of type error typed assigned to a parameter of type `EntityClassOrSchema` @typescript-eslint/no-unsafe-argument
61:11 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
64:15 error Unsafe member access .key on a type that cannot be resolved @typescript-eslint/no-unsafe-member-access
65:45 error The type of computed name [row.key] cannot be resolved @typescript-eslint/no-unsafe-member-access
65:49 error Unsafe member access .key on a type that cannot be resolved @typescript-eslint/no-unsafe-member-access
65:60 error Unsafe member access .value on a type that cannot be resolved @typescript-eslint/no-unsafe-member-access
69:5 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment
77:36 warning Unsafe argument of type error typed assigned to a parameter of type `{ [s: string]: unknown; } | ArrayLike<unknown>` @typescript-eslint/no-unsafe-argument

C:\Users\DELL\Desktop\Arena\InsightArena\backend\src\markets\markets.service.spec.ts
315:7 warning Unsafe argument of type `any` assigned to a parameter of type `SelectQueryBuilder<Market>` @typescript-eslint/no-unsafe-argument
363:7 warning Unsafe argument of type `any` assigned to a parameter of type `SelectQueryBuilder<Market>` @typescript-eslint/no-unsafe-argument

??? 11 problems (7 errors, 4 warnings)

???ELIFECYCLE??? Command failed with exit code 1.
Binary file added backend/lint_final.json
Binary file not shown.
14 changes: 14 additions & 0 deletions backend/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ModerateCommentDto } from './dto/moderate-comment.dto';
import { ReportQueryDto } from './dto/report-query.dto';
import { ResolveMarketDto } from './dto/resolve-market.dto';
import { StatsResponseDto } from './dto/stats-response.dto';
import { UpdateSystemConfigDto } from './dto/system-config.dto';
import { UpdateUserRoleDto } from './dto/update-user-role.dto';

@Controller('admin')
Expand Down Expand Up @@ -161,4 +162,17 @@ export class AdminController {
async getActivityReport(@Query() query: ReportQueryDto) {
return this.adminService.getActivityReport(query);
}

@Get('config')
async getConfig() {
return this.adminService.getConfig();
}

@Patch('config')
async updateConfig(@Body() dto: UpdateSystemConfigDto, @Request() req: any) {
return this.adminService.updateConfig(
dto,
(req as { user: { id: string } }).user.id,
);
}
}
1 change: 1 addition & 0 deletions backend/src/admin/admin.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { AdminService } from './admin.service';
Competition,
CompetitionParticipant,
ActivityLog,
SystemConfig,
]),
FlagsModule,
NotificationsModule,
Expand Down
5 changes: 5 additions & 0 deletions backend/src/admin/admin.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Prediction } from '../predictions/entities/prediction.entity';
import { SorobanService } from '../soroban/soroban.service';
import { User } from '../users/entities/user.entity';
import { AdminService } from './admin.service';
import { SystemConfig } from './entities/system-config.entity';
import { ResolveMarketDto } from './dto/resolve-market.dto';

const mockRepo = () => ({
Expand Down Expand Up @@ -84,6 +85,7 @@ describe('AdminService.adminResolveMarket', () => {
useValue: mockRepo(),
},
{ provide: getRepositoryToken(ActivityLog), useValue: mockRepo() },
{ provide: getRepositoryToken(SystemConfig), useValue: mockRepo() },
{ provide: AnalyticsService, useValue: analyticsService },
{ provide: NotificationsService, useValue: notificationsService },
{ provide: SorobanService, useValue: sorobanService },
Expand Down Expand Up @@ -262,6 +264,7 @@ describe('AdminService.featureMarket', () => {
useValue: mockRepo(),
},
{ provide: getRepositoryToken(ActivityLog), useValue: mockRepo() },
{ provide: getRepositoryToken(SystemConfig), useValue: mockRepo() },
{ provide: AnalyticsService, useValue: analyticsService },
{ provide: NotificationsService, useValue: { create: jest.fn() } },
{ provide: SorobanService, useValue: { resolveMarket: jest.fn() } },
Expand Down Expand Up @@ -361,6 +364,7 @@ describe('AdminService.unfeatureMarket', () => {
useValue: mockRepo(),
},
{ provide: getRepositoryToken(ActivityLog), useValue: mockRepo() },
{ provide: getRepositoryToken(SystemConfig), useValue: mockRepo() },
{ provide: AnalyticsService, useValue: analyticsService },
{ provide: NotificationsService, useValue: { create: jest.fn() } },
{ provide: SorobanService, useValue: { resolveMarket: jest.fn() } },
Expand Down Expand Up @@ -449,6 +453,7 @@ describe('AdminService.updateUserRole', () => {
useValue: mockRepo(),
},
{ provide: getRepositoryToken(ActivityLog), useValue: mockRepo() },
{ provide: getRepositoryToken(SystemConfig), useValue: mockRepo() },
{ provide: AnalyticsService, useValue: analyticsService },
{
provide: NotificationsService,
Expand Down
45 changes: 44 additions & 1 deletion backend/src/admin/admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,16 @@
import { ResolveMarketDto } from './dto/resolve-market.dto';
import { StatsResponseDto } from './dto/stats-response.dto';
import { UpdateUserRoleDto } from './dto/update-user-role.dto';

import { SystemConfig } from './entities/system-config.entity';
import {
DEFAULT_CONFIG,
SystemConfigValues,
UpdateSystemConfigDto,
} from './dto/system-config.dto';
@Injectable()
export class AdminService {
private readonly logger = new Logger(AdminService.name);
private configCache: SystemConfigValues | null = null;

constructor(
@InjectRepository(User)
Expand All @@ -52,12 +58,49 @@
private readonly competitionParticipantsRepository: Repository<CompetitionParticipant>,
@InjectRepository(ActivityLog)
private readonly activityLogsRepository: Repository<ActivityLog>,
@InjectRepository(SystemConfig)
private readonly systemConfigRepository: Repository<SystemConfig>,
private readonly analyticsService: AnalyticsService,
private readonly notificationsService: NotificationsService,
private readonly sorobanService: SorobanService,
private readonly flagsService: FlagsService,
) {}

async getConfig(): Promise<SystemConfigValues> {
if (this.configCache) return this.configCache;

const rows = await this.systemConfigRepository.find();
const config = { ...DEFAULT_CONFIG };

for (const row of rows) {
if (row.key in config) {
(config as Record<string, unknown>)[row.key] = row.value;
}
}

this.configCache = config;
return config;
}

async updateConfig(
dto: UpdateSystemConfigDto,
adminId: string,
): Promise<SystemConfigValues> {
const updates = Object.entries(dto).filter(([, v]) => v !== undefined);

for (const [key, value] of updates) {
await this.systemConfigRepository.save({ key, value });

Check failure on line 92 in backend/src/admin/admin.service.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
}

this.configCache = null;

await this.analyticsService.logActivity(adminId, 'SYSTEM_CONFIG_UPDATED', {
updated_keys: updates.map(([k]) => k),
});

return this.getConfig();
}

async getStats(): Promise<StatsResponseDto> {
const now = new Date();
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
Expand Down
49 changes: 49 additions & 0 deletions backend/src/admin/dto/system-config.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { IsNumber, IsBoolean, IsOptional, Min, Max } from 'class-validator';

export class UpdateSystemConfigDto {
@IsOptional()
@IsNumber()
@Min(0)
@Max(100)
platform_fee_percent?: number;

@IsOptional()
@IsNumber()
@Min(0)
min_stake_stroops?: number;

@IsOptional()
@IsNumber()
@Min(1)
max_markets_per_user?: number;

@IsOptional()
@IsBoolean()
maintenance_mode?: boolean;

@IsOptional()
@IsBoolean()
feature_competitions?: boolean;

@IsOptional()
@IsBoolean()
feature_leaderboard?: boolean;
}

export interface SystemConfigValues {
platform_fee_percent: number;
min_stake_stroops: number;
max_markets_per_user: number;
maintenance_mode: boolean;
feature_competitions: boolean;
feature_leaderboard: boolean;
}

export const DEFAULT_CONFIG: SystemConfigValues = {
platform_fee_percent: 2,
min_stake_stroops: 1000000,
max_markets_per_user: 10,
maintenance_mode: false,
feature_competitions: true,
feature_leaderboard: true,
};
13 changes: 13 additions & 0 deletions backend/src/admin/entities/system-config.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Entity, PrimaryColumn, Column, UpdateDateColumn } from 'typeorm';

@Entity('system_config')
export class SystemConfig {
@PrimaryColumn()
key: string;

@Column('jsonb')
value: unknown;

@UpdateDateColumn()
updated_at: Date;
}
111 changes: 111 additions & 0 deletions backend/src/admin/system-config.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { AnalyticsService } from '../analytics/analytics.service';
import { Competition } from '../competitions/entities/competition.entity';
import { Market } from '../markets/entities/market.entity';
import { Comment } from '../markets/entities/comment.entity';
import { NotificationsService } from '../notifications/notifications.service';
import { Prediction } from '../predictions/entities/prediction.entity';
import { SorobanService } from '../soroban/soroban.service';
import { User } from '../users/entities/user.entity';
import { ActivityLog } from '../analytics/entities/activity-log.entity';
import { AdminService } from './admin.service';
import { SystemConfig } from './entities/system-config.entity';
import { DEFAULT_CONFIG } from './dto/system-config.dto';

const mockRepo = () => ({
findOne: jest.fn(),
save: jest.fn(),
find: jest.fn(),
count: jest.fn(),
createQueryBuilder: jest.fn(),
});

describe('AdminService - system config', () => {
let service: AdminService;
let configRepo: ReturnType<typeof mockRepo>;
let analyticsService: jest.Mocked<Pick<AnalyticsService, 'logActivity'>>;

const adminId = 'admin-1';

beforeEach(async () => {
configRepo = mockRepo();
analyticsService = { logActivity: jest.fn().mockResolvedValue({}) };

const module: TestingModule = await Test.createTestingModule({
providers: [
AdminService,
{ provide: getRepositoryToken(User), useValue: mockRepo() },
{ provide: getRepositoryToken(Market), useValue: mockRepo() },
{ provide: getRepositoryToken(Comment), useValue: mockRepo() },
{ provide: getRepositoryToken(Prediction), useValue: mockRepo() },
{ provide: getRepositoryToken(Competition), useValue: mockRepo() },
{ provide: getRepositoryToken(ActivityLog), useValue: mockRepo() },
{ provide: getRepositoryToken(SystemConfig), useValue: configRepo },
{ provide: AnalyticsService, useValue: analyticsService },
{ provide: NotificationsService, useValue: { create: jest.fn() } },
{ provide: SorobanService, useValue: { resolveMarket: jest.fn() } },
],
}).compile();

service = module.get<AdminService>(AdminService);
});

describe('getConfig', () => {
it('returns defaults when no rows exist', async () => {
configRepo.find.mockResolvedValue([]);
const config = await service.getConfig();
expect(config).toEqual(DEFAULT_CONFIG);
});

it('merges stored values over defaults', async () => {
configRepo.find.mockResolvedValue([
{ key: 'platform_fee_percent', value: 5 },
{ key: 'maintenance_mode', value: true },
]);
const config = await service.getConfig();
expect(config.platform_fee_percent).toBe(5);
expect(config.maintenance_mode).toBe(true);
expect(config.min_stake_stroops).toBe(DEFAULT_CONFIG.min_stake_stroops);
});

it('returns cached value on second call', async () => {
configRepo.find.mockResolvedValue([]);
await service.getConfig();
await service.getConfig();
expect(configRepo.find).toHaveBeenCalledTimes(1);
});
});

describe('updateConfig', () => {
it('saves each provided key and invalidates cache', async () => {
configRepo.find.mockResolvedValue([]);
configRepo.save.mockResolvedValue({});

await service.updateConfig({ platform_fee_percent: 3 }, adminId);

expect(configRepo.save).toHaveBeenCalledWith({
key: 'platform_fee_percent',
value: 3,
});
expect(analyticsService.logActivity).toHaveBeenCalledWith(
adminId,
'SYSTEM_CONFIG_UPDATED',
expect.objectContaining({ updated_keys: ['platform_fee_percent'] }),
);
});

it('does not save keys with undefined values', async () => {
configRepo.find.mockResolvedValue([]);
configRepo.save.mockResolvedValue({});

await service.updateConfig({ maintenance_mode: true }, adminId);

expect(configRepo.save).toHaveBeenCalledTimes(1);
expect(configRepo.save).toHaveBeenCalledWith({
key: 'maintenance_mode',
value: true,
});
});
});
});
15 changes: 15 additions & 0 deletions backend/src/migrations/1774600000000-CreateSystemConfigEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class CreateSystemConfigEntity1774600000000 implements MigrationInterface {
name = 'CreateSystemConfigEntity1774600000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "system_config" ("key" character varying NOT NULL, "value" jsonb NOT NULL, "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_system_config" PRIMARY KEY ("key"))`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "system_config"`);
}
}
Binary file added backend/test_out.txt
Binary file not shown.
Loading