Skip to content

Commit 8d476d2

Browse files
authored
Fix CAPTCHA bypass, cursor pagination, explorer links, and blockchain queue (#874, #873, #872, #871) (#979)
* fix: resolve CAPTCHA bypass, cursor pagination, explorer links, and blockchain queue issues - Fix CAPTCHA verification bypass in non-production mode (#874): Require CAPTCHA by default in all environments, add CAPTCHA_BYPASS env var for dev-only bypass - Implement cursor-based pagination for large datasets (#873): Add cursor support to search, admin, and transactions services with backward compatibility - Fix blockchain explorer link generation (#872): Use blockchainHash instead of truncated UUID for from address - Fix blockchain recording processor queue name mismatch (#871): Align processor queue name with service, remove non-existent submitTransaction call * Fix remaining TypeScript errors - Fix issueTokenPair parameter type to accept Prisma User type - Add optional chaining for apiKeyAnalyticsService - Fix error type in catch handler for API key usage - Cast getSuggestions return type to string[] --------- Co-authored-by: khaadish <khaadish@users.noreply.github.com>
1 parent cca1823 commit 8d476d2

9 files changed

Lines changed: 273 additions & 94 deletions

File tree

‎.env.example‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ GEOCODING_USER_AGENT=PropChain-Backend/1.0 (geocoding)
6161
GEOCODING_TIMEOUT_MS=5000
6262
# GOOGLE_GEOCODING_API_KEY=
6363

64+
# CAPTCHA Configuration
65+
# Set to 'true' only in development environments to bypass CAPTCHA verification
66+
CAPTCHA_BYPASS=false
67+
6468
# Redis (used by Docker Compose and cache module)
6569
REDIS_HOST=localhost
6670
REDIS_PORT=6379

‎pr_body.txt‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## Changes
2+
3+
### Add proper CSP headers and security middleware (#870)
4+
- Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security, Referrer-Policy, Permissions-Policy
5+
6+
### Implement proper CORS configuration (#869)
7+
- Use CORS_ORIGINS env var instead of wildcard
8+
- Refuse wildcard origins in production mode
9+
10+
### Fix CAPTCHA verification bypass in non-production mode (#868)
11+
- Require CAPTCHA by default in all environments
12+
- Add CAPTCHA_BYPASS env var for development-only bypass
13+
- Log warning when CAPTCHA is bypassed
14+
15+
### Add distributed tracing via OpenTelemetry TraceInterceptor (#808)
16+
- UUID trace IDs for request tracking
17+
- Request start/completion/failure logging with duration
18+
- Global interceptor for all HTTP requests

‎src/admin/admin.service.ts‎

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
// @ts-nocheck
2-
3-
import { Injectable, NotFoundException } from '@nestjs/common';
1+
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
42
import { PrismaService } from '../database/prisma.service';
53
import { BackupService } from '../backup/backup.service';
64
import { UpdateBackupScheduleDto } from '../backup/dto/backup.dto';
@@ -22,6 +20,8 @@ import { FraudService } from '../fraud/fraud.service';
2220
import { SessionsService } from '../sessions/sessions.service';
2321
import { TransactionsService } from '../transactions/transactions.service';
2422

23+
const logger = new Logger('AdminService');
24+
2525
@Injectable()
2626
export class AdminService {
2727
constructor(
@@ -109,9 +109,11 @@ export class AdminService {
109109
async listUsers(query: AdminUsersQueryDto) {
110110
const page = query.page ?? 1;
111111
const limit = query.limit ?? 20;
112-
const skip = (page - 1) * limit;
112+
const skip = (query as any).cursor
113+
? undefined
114+
: (page - 1) * limit;
113115

114-
const where = {
116+
const where: any = {
115117
role: query.role,
116118
OR: query.search
117119
? [
@@ -122,10 +124,14 @@ export class AdminService {
122124
: undefined,
123125
};
124126

127+
if ((query as any).cursor) {
128+
where.createdAt = { lt: new Date(Buffer.from((query as any).cursor, 'base64').toString()) };
129+
}
130+
125131
const [items, total] = await Promise.all([
126132
this.prisma.user.findMany({
127133
where,
128-
skip,
134+
...(skip !== undefined ? { skip } : {}),
129135
take: limit,
130136
orderBy: { createdAt: 'desc' },
131137
select: {
@@ -142,13 +148,17 @@ export class AdminService {
142148
this.prisma.user.count({ where }),
143149
]);
144150

145-
return { total, page, limit, items };
151+
const nextCursor = items.length === limit
152+
? Buffer.from((items[items.length - 1] as any).createdAt.toISOString()).toString('base64')
153+
: null;
154+
155+
return { total, page, limit, items, nextCursor, previousCursor: (query as any).cursor || null };
146156
}
147157

148-
async updateUser(userId: string, payload: AdminUpdateUserDto) {
158+
async updateUser(userId: string, payload: AdminUpdateUserDto, actorId?: string) {
149159
const existingUser = await this.prisma.user.findUnique({
150160
where: { id: userId },
151-
select: { role: true },
161+
select: { role: true, email: true },
152162
});
153163

154164
if (!existingUser) {
@@ -171,10 +181,48 @@ export class AdminService {
171181
},
172182
});
173183

184+
// Audit log for role changes (#886)
174185
if (payload.role && payload.role !== existingUser.role) {
186+
await this.prisma.activityLog
187+
.create({
188+
data: {
189+
userId: actorId ?? 'system',
190+
action: 'ROLE_CHANGE',
191+
entityType: 'USER',
192+
entityId: userId,
193+
description: `Role changed from ${existingUser.role} to ${payload.role} for user ${existingUser.email}`,
194+
metadata: {
195+
previousRole: existingUser.role,
196+
newRole: payload.role,
197+
targetUserId: userId,
198+
},
199+
},
200+
})
201+
.catch((err: unknown) => {
202+
logger.error(`Failed to audit-log role change for ${userId}: ${err}`);
203+
});
204+
175205
await this.sessionsService.revokeAllSessions(userId);
176206
}
177207

208+
// Audit log for block state changes
209+
if (payload.isBlocked !== undefined) {
210+
await this.prisma.activityLog
211+
.create({
212+
data: {
213+
userId: actorId ?? 'system',
214+
action: payload.isBlocked ? 'USER_BLOCKED' : 'USER_UNBLOCKED',
215+
entityType: 'USER',
216+
entityId: userId,
217+
description: `User ${existingUser.email} ${payload.isBlocked ? 'blocked' : 'unblocked'}`,
218+
metadata: { targetUserId: userId, isBlocked: payload.isBlocked },
219+
},
220+
})
221+
.catch((err: unknown) => {
222+
logger.error(`Failed to audit-log block state for ${userId}: ${err}`);
223+
});
224+
}
225+
178226
return updatedUser;
179227
}
180228

@@ -193,16 +241,22 @@ export class AdminService {
193241
async getModerationQueue(query: ModerationQueueQueryDto) {
194242
const page = query.page ?? 1;
195243
const limit = query.limit ?? 20;
196-
const skip = (page - 1) * limit;
244+
const skip = (query as any).cursor
245+
? undefined
246+
: (page - 1) * limit;
197247

198-
const where = {
248+
const where: any = {
199249
status: query.status ?? PropertyStatus.PENDING,
200250
};
201251

252+
if ((query as any).cursor) {
253+
where.createdAt = { lt: new Date(Buffer.from((query as any).cursor, 'base64').toString()) };
254+
}
255+
202256
const [items, total] = await Promise.all([
203257
this.prisma.property.findMany({
204258
where,
205-
skip,
259+
...(skip !== undefined ? { skip } : {}),
206260
take: limit,
207261
orderBy: { createdAt: 'asc' },
208262
include: {
@@ -219,7 +273,11 @@ export class AdminService {
219273
this.prisma.property.count({ where }),
220274
]);
221275

222-
return { total, page, limit, items };
276+
const nextCursor = items.length === limit
277+
? Buffer.from((items[items.length - 1] as any).createdAt.toISOString()).toString('base64')
278+
: null;
279+
280+
return { total, page, limit, items, nextCursor, previousCursor: (query as any).cursor || null };
223281
}
224282

225283
async approveProperty(propertyId: string) {
@@ -292,18 +350,24 @@ export class AdminService {
292350
async monitorTransactions(query: TransactionMonitoringQueryDto) {
293351
const page = query.page ?? 1;
294352
const limit = query.limit ?? 20;
295-
const skip = (page - 1) * limit;
353+
const skip = (query as any).cursor
354+
? undefined
355+
: (page - 1) * limit;
296356

297-
const where = {
357+
const where: any = {
298358
status: query.status,
299359
type: query.type,
300360
propertyId: query.propertyId,
301361
};
302362

363+
if ((query as any).cursor) {
364+
where.createdAt = { lt: new Date(Buffer.from((query as any).cursor, 'base64').toString()) };
365+
}
366+
303367
const [items, total] = await Promise.all([
304368
this.prisma.transaction.findMany({
305369
where,
306-
skip,
370+
...(skip !== undefined ? { skip } : {}),
307371
take: limit,
308372
orderBy: { createdAt: 'desc' },
309373
include: {
@@ -321,7 +385,11 @@ export class AdminService {
321385
this.prisma.transaction.count({ where }),
322386
]);
323387

324-
return { total, page, limit, items };
388+
const nextCursor = items.length === limit
389+
? Buffer.from((items[items.length - 1] as any).createdAt.toISOString()).toString('base64')
390+
: null;
391+
392+
return { total, page, limit, items, nextCursor, previousCursor: (query as any).cursor || null };
325393
}
326394

327395
async transactionMonitoringSummary() {

‎src/admin/dto/admin.dto.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ export class AdminUpdateUserDto {
6868

6969
@IsOptional()
7070
isVerified?: boolean;
71+
72+
@IsOptional()
73+
isBlocked?: boolean;
7174
}
7275

7376
export class ModerationQueueQueryDto {

0 commit comments

Comments
 (0)