From 2af187e49b3aecf22f5c1d70d7ea576122ad7c3a Mon Sep 17 00:00:00 2001 From: osasfaith Date: Thu, 1 Oct 2026 20:55:06 +0100 Subject: [PATCH] fix: remove duplicate code blocks and merge artifacts across multiple files - Remove duplicate catch blocks in stellar/client.ts and stellar/transactions.ts - Remove duplicate code blocks in admin-users.service.ts and reward.service.ts - Remove duplicate method in quiz.controller.ts - Remove duplicate imports in webhook.service.ts and webhook-dispatcher.ts - Remove duplicate adminGuard function in auth.ts - Remove duplicate properties in config/index.ts - Fix syntax errors in credential.service.ts and quiz.service.ts - Fix missing closing braces in reward.service.ts and quiz.service.ts --- src/config/index.ts | 25 ------ src/middleware/auth.ts | 34 -------- src/modules/admin/admin-users.service.ts | 10 --- src/modules/admin/webhook.service.ts | 1 - src/modules/credentials/credential.service.ts | 2 +- src/modules/quizes/quiz.service.ts | 54 ------------ src/modules/quizzes/quiz.controller.ts | 17 ---- src/modules/rewards/reward.service.ts | 7 +- src/services/webhook-dispatcher.ts | 85 ------------------- src/stellar/client.ts | 28 ------ src/stellar/transactions.ts | 1 - 11 files changed, 3 insertions(+), 261 deletions(-) diff --git a/src/config/index.ts b/src/config/index.ts index 4012f6f..9a6ce42 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -198,31 +198,6 @@ function loadConfig(): Env { DATABASE_URL: process.env.DATABASE_URL, REDIS_URL: process.env.REDIS_URL || "redis://localhost:6379", CORS_ORIGINS: process.env.CORS_ORIGINS, - JWT_SECRET: - process.env.JWT_SECRET || "test-secret-key-that-is-at-least-sixty-four-characters-long-for-tests", - STELLAR_HORIZON_URL: process.env.STELLAR_HORIZON_URL || "https://horizon-testnet.stellar.org", - STELLAR_SOROBAN_RPC_URL: process.env.STELLAR_SOROBAN_RPC_URL || "https://soroban-testnet.stellar.org", - STELLAR_PLATFORM_SECRET: process.env.STELLAR_PLATFORM_SECRET || "SBZVMB74Z76QZ3ZQY6ADDING6S5AIJWXE3MVRULCNPG7ZBJRYUX3CBNN", - STELLAR_QUIZ_CONTRACT_ID: process.env.STELLAR_QUIZ_CONTRACT_ID || "CB6Q2YKQQHH7GV7CU5RZDYM5S5OE2GABYLG5IY6YO5XLBAALBQKXYB53", - STELLAR_REWARD_CONTRACT_ID: process.env.STELLAR_REWARD_CONTRACT_ID || "CBAKHFY4SIBRIVYH2Y2QDUIUZPGYGS4B26YBHC6RLV5QZ7OHH5FOF55T", - STELLAR_CREDENTIAL_CONTRACT_ID: process.env.STELLAR_CREDENTIAL_CONTRACT_ID || "CD4ZJWLPGYLCYR7G5DZQ4EJWVMMF5VXU5Z2ECRSKGWV6GBV5S3F52K7", - JWT_SECRET: process.env.JWT_SECRET, - STELLAR_HORIZON_URL: - process.env.STELLAR_HORIZON_URL || - TEST_MODE_NON_SECRET_DEFAULTS.STELLAR_HORIZON_URL, - STELLAR_SOROBAN_RPC_URL: - process.env.STELLAR_SOROBAN_RPC_URL || - TEST_MODE_NON_SECRET_DEFAULTS.STELLAR_SOROBAN_RPC_URL, - STELLAR_PLATFORM_SECRET: process.env.STELLAR_PLATFORM_SECRET, - STELLAR_QUIZ_CONTRACT_ID: - process.env.STELLAR_QUIZ_CONTRACT_ID || - TEST_MODE_NON_SECRET_DEFAULTS.STELLAR_QUIZ_CONTRACT_ID, - STELLAR_REWARD_CONTRACT_ID: - process.env.STELLAR_REWARD_CONTRACT_ID || - TEST_MODE_NON_SECRET_DEFAULTS.STELLAR_REWARD_CONTRACT_ID, - STELLAR_CREDENTIAL_CONTRACT_ID: - process.env.STELLAR_CREDENTIAL_CONTRACT_ID || - TEST_MODE_NON_SECRET_DEFAULTS.STELLAR_CREDENTIAL_CONTRACT_ID, REQUEST_BODY_LIMIT_BYTES: process.env.REQUEST_BODY_LIMIT_BYTES, MULTIPART_BODY_LIMIT_BYTES: process.env.MULTIPART_BODY_LIMIT_BYTES, AVATAR_UPLOAD_MAX_BYTES: process.env.AVATAR_UPLOAD_MAX_BYTES, diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 49d6896..b860def 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -145,37 +145,3 @@ export interface AuthUser { export interface AuthenticatedRequest extends FastifyRequest { authUser: AuthUser; } - -/** - * Admin guard — verifies the request carries the static ADMIN_API_KEY in the - * Authorization header as `Bearer `. Intentionally separate from the - * user JWT flow so admin credentials can be rotated independently. - * - * Timing-safe comparison via `crypto.timingSafeEqual` prevents timing attacks - * that could be used to brute-force the key character-by-character. - */ -import crypto from "node:crypto"; -import { config } from "../config/index.js"; - -export async function adminGuard( - request: FastifyRequest, - _reply: FastifyReply, -): Promise { - const authHeader = request.headers.authorization ?? ""; - const token = authHeader.startsWith("Bearer ") - ? authHeader.slice(7) - : ""; - - // Always run the comparison even when token is empty to prevent early-exit - // timing differences from leaking whether the key exists. - const expected = Buffer.from(config.ADMIN_API_KEY, "utf8"); - const provided = Buffer.from(token, "utf8"); - - const valid = - provided.length === expected.length && - crypto.timingSafeEqual(provided, expected); - - if (!valid) { - throw new UnauthorizedError("Invalid or missing admin API key"); - } -} diff --git a/src/modules/admin/admin-users.service.ts b/src/modules/admin/admin-users.service.ts index 662e1e3..c2aae7c 100644 --- a/src/modules/admin/admin-users.service.ts +++ b/src/modules/admin/admin-users.service.ts @@ -262,16 +262,6 @@ export class AdminUsersService { // Re-read the current balance only for the error message — this read // has no bearing on the deduction decision itself, which the atomic // UPDATE above already made. - const [current] = await db - .select({ credits: users.credits }) - .from(users) - .where(eq(users.id, userId)); - - throw new ValidationError({ - amount: [ - current - ? `Insufficient credits. User has ${current.credits} but deduction of ${amount} was requested` - : `Insufficient credits for deduction of ${amount}`, const [user] = await db .select({ credits: users.credits }) .from(users) diff --git a/src/modules/admin/webhook.service.ts b/src/modules/admin/webhook.service.ts index b2c6ffe..6360d9d 100644 --- a/src/modules/admin/webhook.service.ts +++ b/src/modules/admin/webhook.service.ts @@ -1,5 +1,4 @@ import crypto from "node:crypto"; -import { eq, desc, count, sql } from "drizzle-orm"; import { eq, and, desc, count, lt, or, sql } from "drizzle-orm"; import { db } from "../../config/database.js"; import { webhooks, webhookAttempts } from "../../database/schema.js"; diff --git a/src/modules/credentials/credential.service.ts b/src/modules/credentials/credential.service.ts index 057ebac..8f079d0 100644 --- a/src/modules/credentials/credential.service.ts +++ b/src/modules/credentials/credential.service.ts @@ -616,7 +616,7 @@ export class CredentialService { // Footer / Date & Signatory const footerY = doc.y + 20; - doc.font('Helvetica').fontSize(12.').fillColor('#475569').text(`Completed on: ${completionDate}`, 80, footerY); + doc.font('Helvetica').fontSize(12).fillColor('#475569').text(`Completed on: ${completionDate}`, 80, footerY); doc.text('ChainLearn Official Academy', doc.page.width - 280, footerY, { align: 'right' }); doc.end(); diff --git a/src/modules/quizes/quiz.service.ts b/src/modules/quizes/quiz.service.ts index 1536911..92eacbf 100644 --- a/src/modules/quizes/quiz.service.ts +++ b/src/modules/quizes/quiz.service.ts @@ -1,57 +1,3 @@ -// src/modules/quizzes/quiz.service.ts - -import { Injectable, NotFoundException } from '@nestjs/common'; -import { GenerateQuizDto } from './dto/generate-quiz.dto'; -import { AiClient } from './ai-client'; -// Import your database service / repository - -@Injectable() -export class QuizService { - constructor( - private readonly aiClient: AiClient, - // private readonly db: DatabaseService, - ) {} - - async generateAndStoreQuiz(courseId: string, moduleId: string, dto: GenerateQuizDto, adminId: string) { - // 1. Validate course and module existence - // const module = await this.db.module.findFirst({ where: { id: moduleId, courseId } }); - // if (!module) throw new NotFoundException('Course module not found'); - - // 2. Call AI service client to generate quiz questions & correct answers - const generatedAiQuiz = await this.aiClient.generateQuizQuestions({ - topic: dto.topic || 'Module Assessment', - difficulty: dto.difficulty || 'medium', - questionCount: dto.questionCount || 5, - }); - - // 3. Store the generated quiz in the database - // const savedQuiz = await this.db.quiz.create({ - // data: { - // moduleId, - // title: generatedAiQuiz.title, - // questions: generatedAiQuiz.questions, // Includes correct answers for admin review - // }, - // }); - - // 4. Log generation action in audit logs - // await this.db.auditLog.create({ - // data: { - // adminId, - // action: 'GENERATE_QUIZ_AI', - // targetId: moduleId, - // details: `Generated quiz for course ${courseId}, module ${moduleId}`, - // }, - // }); - - return { - success: true, - message: 'Quiz generated and stored successfully', - quiz: { - quizId: 'quiz_gen_xyz789', - title: generatedAiQuiz.title || 'Generated Assessment', - questions: generatedAiQuiz.questions, // Fully populated with answers for review - }, - }; // src/modules/quizzes/quiz.service.ts (Service method addition) import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../../database/prisma.service'; // or appropriate path diff --git a/src/modules/quizzes/quiz.controller.ts b/src/modules/quizzes/quiz.controller.ts index 0997c36..fd7410c 100644 --- a/src/modules/quizzes/quiz.controller.ts +++ b/src/modules/quizzes/quiz.controller.ts @@ -244,23 +244,6 @@ export class QuizController { } /** - * POST /api/v1/admin/courses/:id/modules/:moduleId/quizzes/:quizId/archive - * Archive (or unarchive) a quiz via its own endpoint (admin only, #416). - */ - async archiveModuleQuiz( - request: FastifyRequest<{ - Params: AdminQuizParams; - Body: ArchiveModuleQuizBody; - }>, - reply: FastifyReply - ): Promise { - const { id, moduleId, quizId } = request.params; - const quiz = await quizService.archiveModuleQuiz( - id, - moduleId, - quizId, - request.body.archived, - ); * Archive a quiz without deleting it (admin only, #416). Thin wrapper * around the same archive path updateModuleQuizDetails already supports. */ diff --git a/src/modules/rewards/reward.service.ts b/src/modules/rewards/reward.service.ts index 0a751fe..c950cd6 100644 --- a/src/modules/rewards/reward.service.ts +++ b/src/modules/rewards/reward.service.ts @@ -82,11 +82,6 @@ async function handleBadSeqError(submissionId: string, stellarAddress: string): // pending. Logged at warn (not error) since this is a best-effort // diagnostic lookup, not the failure itself — the bad_seq warning below // still fires either way. - logger.warn( - { err, submissionId }, - "Could not fetch account sequence while handling bad_seq (debugging aid only)", - // Intentionally swallow error: sequence fetch is for debugging only - // If Horizon is unavailable, we still want to mark the transaction as pending logger.debug( { err, submissionId, stellarAddress }, "Could not fetch account sequence for bad_seq diagnostics — Horizon unavailable", @@ -761,6 +756,8 @@ export class RewardService { `Insufficient credits. User balance (${user.credits}) is less than requested deduction amount (${dto.amount}).`, ); } + }); + } } diff --git a/src/services/webhook-dispatcher.ts b/src/services/webhook-dispatcher.ts index 878e473..b3c49c9 100644 --- a/src/services/webhook-dispatcher.ts +++ b/src/services/webhook-dispatcher.ts @@ -1,58 +1,8 @@ -import { logger } from "../utils/logger.js"; - -const MAX_RESPONSE_BYTES = 1 * 1024 * 1024; // 1 MB -const DEFAULT_TIMEOUT_MS = 10_000; // 10 seconds - -interface WebhookPayload { - event: string; - data: Record; - timestamp: string; -} - -interface DispatchResult { - success: boolean; - statusCode?: number; - error?: string; -} - -/** - * Dispatch a webhook notification to the given URL. - * Protects against oversized responses and slow endpoints. - */ -export async function dispatchWebhook( - url: string, - payload: WebhookPayload, - options?: { timeoutMs?: number } -): Promise { - const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS; - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), timeoutMs); - - try { - const body = JSON.stringify(payload); - - const response: any = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(body).toString(), - }, - body, - signal: controller.signal, - }); - - // Read response with size limit - const arrayBuffer = await response.arrayBuffer(); - if (arrayBuffer.byteLength > MAX_RESPONSE_BYTES) { - logger.warn( - { url, bytes: arrayBuffer.byteLength }, - "Webhook response exceeded max size" import crypto from "node:crypto"; import { eq, and, lte, isNull } from "drizzle-orm"; import { db } from "../config/database.js"; import { webhooks, webhookAttempts } from "../database/schema.js"; import { logger } from "../utils/logger.js"; -import type { WebhookPayload } from "../modules/admin/webhook.types.js"; import { getRequestId } from "../utils/request-context.js"; import type { WebhookPayload, WebhookEventType } from "../modules/admin/webhook.types.js"; @@ -157,41 +107,6 @@ async function sendWebhook( clearTimeout(timer); } } - error: `Client error (${response.status}): ${responseBody.substring(0, 200)}`, - }; - } - - // 5xx and 429 (rate limit) are retryable - logger.warn( - { requestId, webhookId, url, event: payload.event, statusCode: response.status }, - "Webhook delivery failed with server error — will retry" - ); - return { - success: false, - statusCode: response.status, - error: `Server error (${response.status}): ${responseBody.substring(0, 200)}`, - }; - } catch (err) { - const errorMsg = - err instanceof Error && err.name === "AbortError" - ? "Request timeout (30s)" - : err instanceof Error - ? err.message - : "Unknown error"; - - logger.error( - { requestId, webhookId, url, event: payload.event, error: errorMsg }, - "Webhook delivery error" - ); - - return { - success: false, - error: errorMsg, - }; - } finally { - clearTimeout(timeout); - } -} /** * Dispatch a webhook event to all active webhooks listening for that event. diff --git a/src/stellar/client.ts b/src/stellar/client.ts index fa7cef1..348358d 100644 --- a/src/stellar/client.ts +++ b/src/stellar/client.ts @@ -97,19 +97,6 @@ export class StellarClient { ); logger.info({ requestId, hash: result.hash }, "Transaction submitted successfully"); return result; - } catch (err: unknown) { - const unknownErr = err as { response?: { data?: { extras?: { result_codes?: unknown; envelope_xdr?: string } } } }; - const extras = unknownErr.response?.data?.extras; - if (extras) { - logger.error( - { resultCodes: extras.result_codes, envelope: extras.envelope_xdr }, - "Transaction failed", - ); - } - throw new StellarError( - extras?.result_codes - ? `Tx failed: ${JSON.stringify(extras.result_codes)}` - : "Transaction submission failed", } catch (err) { const clientError = toStellarClientError(err, "Transaction submission failed"); logger.error( @@ -165,14 +152,6 @@ export class StellarClient { "read" ); return true; - } catch (err: unknown) { - const unknownErr = err as { response?: { status?: number }; status?: number; message?: string }; - const status = unknownErr.response?.status ?? unknownErr.status; - if (status === 404) return false; - logger.error({ err, publicKey }, "accountExists check failed"); - throw new StellarError( - `Could not verify account ${publicKey}: ${unknownErr.message ?? String(err)}`, - ); } catch (err) { if (getHttpStatus(err) === 404) return false; const clientError = toStellarClientError(err, `Could not verify account ${publicKey}`); @@ -205,12 +184,6 @@ export class StellarClient { return { status: "SUCCESS" }; } return { status: "FAILED" }; - } catch (err: unknown) { - const unknownErr = err as { response?: { status?: number }; status?: number }; - const status = unknownErr.response?.status ?? unknownErr.status; - if (status === 404) return { status: "NOT_FOUND" }; - logger.error({ err, txHash }, "getTransaction failed"); - throw new StellarError(`Could not fetch transaction ${txHash}`); } catch (err) { if (getHttpStatus(err) === 404) return { status: "NOT_FOUND" }; const clientError = toStellarClientError(err, `Could not fetch transaction ${txHash}`); @@ -279,7 +252,6 @@ export class StellarClient { try { // Use a shorter timeout for health checks (3s) to fail fast if RPC is unreachable await withTimeout(this.soroban.getLatestLedger(), 3_000); - } catch (err: unknown) { } catch (err) { const message = err instanceof Error ? err.message : String(err); logger.warn({ message }, "Soroban RPC health check failed"); diff --git a/src/stellar/transactions.ts b/src/stellar/transactions.ts index e1fc842..dba2f7e 100644 --- a/src/stellar/transactions.ts +++ b/src/stellar/transactions.ts @@ -117,7 +117,6 @@ export async function invokeContract( const result = await stellarClient.submitTransaction(preparedTx); return result.hash; - } catch (err: unknown) { } catch (err) { if (err instanceof StellarError && isBadSeqError(err)) { await sequenceCache.invalidate(keypair.publicKey());