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
25 changes: 0 additions & 25 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
34 changes: 0 additions & 34 deletions src/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <key>`. 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<void> {
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");
}
}
10 changes: 0 additions & 10 deletions src/modules/admin/admin-users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion src/modules/admin/webhook.service.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/modules/credentials/credential.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
54 changes: 0 additions & 54 deletions src/modules/quizes/quiz.service.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
17 changes: 0 additions & 17 deletions src/modules/quizzes/quiz.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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.
*/
Expand Down
7 changes: 2 additions & 5 deletions src/modules/rewards/reward.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -761,6 +756,8 @@ export class RewardService {
`Insufficient credits. User balance (${user.credits}) is less than requested deduction amount (${dto.amount}).`,
);
}
});
}
}


Expand Down
85 changes: 0 additions & 85 deletions src/services/webhook-dispatcher.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;
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<DispatchResult> {
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";

Expand Down Expand Up @@ -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.
Expand Down
28 changes: 0 additions & 28 deletions src/stellar/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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}`);
Expand Down Expand Up @@ -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");
Expand Down
1 change: 0 additions & 1 deletion src/stellar/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down