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
6 changes: 1 addition & 5 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
"dev": "nodemon src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
{
"scripts": {
"test": "node --require ts-node/register --test tests/**/*.test.ts"
}
}
},
"keywords": [],
"author": "",
Expand Down Expand Up @@ -51,4 +47,4 @@
"ts-node": "^10.9.2",
"typescript": "^6.0.3"
}
}
}
22 changes: 18 additions & 4 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import cookieParser from "cookie-parser";
import crypto from "crypto";
import dotenv from "dotenv";
import { prisma, connectWithRetry, startPoolHealthCheck } from "./config/db";
import { redis } from "./config/redis";
import { trace } from "./config/tracing";
import { intakeRateLimit } from "./middleware/intakeRateLimit";
import { sqlInjectionGuard } from "./middleware/sanitize";
Expand Down Expand Up @@ -43,9 +44,9 @@ app.use(express.json());
// CSRF protection middleware (double-submit cookie pattern)
const csrfMiddleware = (req: Request, res: Response, next: NextFunction) => {
// Skip CSRF for GET/HEAD/OPTIONS and auth challenge/verify routes
if (["GET", "HEAD", "OPTIONS"].includes(req.method) ||
(req.path.startsWith("/api/v1/auth/") &&
(req.path.endsWith("/challenge") || req.path.endsWith("/verify")))) {
if (["GET", "HEAD", "OPTIONS"].includes(req.method) ||
(req.path.startsWith("/api/v1/auth/") &&
(req.path.endsWith("/challenge") || req.path.endsWith("/verify")))) {
return next();
}

Expand All @@ -63,7 +64,7 @@ const csrfMiddleware = (req: Request, res: Response, next: NextFunction) => {
// Route to get CSRF token
app.get("/api/v1/auth/csrf", (req: Request, res: Response) => {
const csrfToken = crypto.randomBytes(32).toString("hex");

// Set CSRF cookie (HttpOnly false so frontend can read it, SameSite strict)
res.cookie(CSRF_COOKIE_NAME, csrfToken, {
httpOnly: false,
Expand Down Expand Up @@ -162,6 +163,12 @@ process.on("SIGTERM", async () => {
stopStorageCleanup();
try {
await prisma.$disconnect();
try {
await redis.quit();
logger.info("Redis connection closed");
} catch (err: any) {
logger.debug("Error closing Redis on shutdown:", err?.message || err);
}
logger.info("Database connection closed");
process.exit(0);
} catch (error) {
Expand All @@ -179,6 +186,13 @@ process.on("SIGTERM", async () => {
async function bootstrap(): Promise<void> {
try {
await connectWithRetry();
// Ensure Redis client is connected (lazyConnect enabled in config)
try {
await redis.connect();
console.log("[redis] connected (bootstrap)");
} catch (err: any) {
console.warn("[redis] could not connect at startup:", err?.message || err);
}
startPoolHealthCheck();
startStorageCleanup();
app.listen(port, () => {
Expand Down
12 changes: 5 additions & 7 deletions backend/src/routes/pool-enhanced.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ router.get("/health", async (req: Request, res: Response) => {
const uptime = stats.uptimeSeconds || 0;

// Determine overall health status
const isPrimary Healthy = stats.primaryStatus === "healthy";
const isPrimaryHealthy = stats.primaryStatus === "healthy";
const hasHealthyReplicas = (stats.replicaStatuses || []).some(
(s: string) => s === "healthy"
);
Expand Down Expand Up @@ -95,8 +95,8 @@ router.get("/health", async (req: Request, res: Response) => {
connectionPoolUtilization:
stats.totalConnections && stats.totalConnections > 0
? ((stats.totalConnections - (stats.idleConnections || 0)) /
stats.totalConnections) *
100
stats.totalConnections) *
100
: 0,
uptime: uptime,
timestamp: Date.now(),
Expand Down Expand Up @@ -141,8 +141,7 @@ router.get("/stats", async (req: Request, res: Response) => {
``,
`# HELP lance_pool_active_connections Active database pool connections`,
`# TYPE lance_pool_active_connections gauge`,
`lance_pool_active_connections ${
(stats.totalConnections || 0) - (stats.idleConnections || 0)
`lance_pool_active_connections ${(stats.totalConnections || 0) - (stats.idleConnections || 0)
}`,
``,
`# HELP lance_pool_waiting_requests Requests waiting for connection`,
Expand All @@ -155,8 +154,7 @@ router.get("/stats", async (req: Request, res: Response) => {
``,
`# HELP lance_pool_health_checks_total Total health checks performed`,
`# TYPE lance_pool_health_checks_total counter`,
`lance_pool_health_checks_total ${
(stats.successfulHealthChecks || 0) + (stats.failedHealthChecks || 0)
`lance_pool_health_checks_total ${(stats.successfulHealthChecks || 0) + (stats.failedHealthChecks || 0)
}`,
``,
`# HELP lance_pool_health_check_failures_total Failed health checks`,
Expand Down
96 changes: 96 additions & 0 deletions backend/test/run-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import assert from "assert";
import jwt from "jsonwebtoken";
import crypto from "crypto";

// Ensure ts-node can run this file even if project modules expect env vars
process.env.JWT_SECRET = process.env.JWT_SECRET || "test-secret";

// Lightweight mock Redis implementation
class MockRedis {
store: Map<string, { value: string; expiresAt?: number }> = new Map();
lastSetArgs: any = null;
async connect() { }
async quit() { }
async set(key: string, value: string, ...args: any[]) {
// Parse args for EX <seconds> and NX
let exSeconds: number | undefined = undefined;
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (a === "EX") {
exSeconds = Number(args[i + 1]);
}
}
const expiresAt = exSeconds ? Math.floor(Date.now() / 1000) + exSeconds : undefined;
this.store.set(key, { value, expiresAt });
this.lastSetArgs = { key, value, exSeconds, args };
return "OK";
}
async get(key: string) {
const v = this.store.get(key);
if (!v) return null;
if (v.expiresAt && v.expiresAt < Math.floor(Date.now() / 1000)) {
this.store.delete(key);
return null;
}
return v.value;
}
}

async function main() {
// Replace the real Redis client with our mock before importing modules
const redisModulePath = "../src/config/redis";
const redisModule = require(redisModulePath);
const mock = new MockRedis();
redisModule.redis = mock;

const auth = require("../src/routes/auth");
const authGuardModule = require("../src/middleware/authGuard");

// Test 1: blacklistToken stores the key and isTokenBlacklisted reads it
const jti = "test-jti-1";
const expiresAt = Math.floor(Date.now() / 1000) + 60; // expires in 60s
await auth.blacklistToken(jti, expiresAt);
assert(mock.lastSetArgs, "redis.set should have been called");
assert.strictEqual(mock.lastSetArgs.key, `jwt:blacklist:${jti}`);

const blacklisted = await auth.isTokenBlacklisted(jti);
assert.strictEqual(blacklisted, true, "Token should be reported blacklisted");

// Test 2: TTL matches remaining seconds approximately
const recordedTtl = mock.lastSetArgs.exSeconds;
const expected = expiresAt - Math.floor(Date.now() / 1000);
assert(Math.abs(recordedTtl - expected) <= 2, `TTL should match remaining lifetime (got ${recordedTtl}, expected ${expected})`);

// Test 3: authGuard rejects a request when token jti is blacklisted
// Create an access token with the same jti and appropriate claims
const secret = process.env.JWT_SECRET as string;
const token = jwt.sign({ address: "GABCDEFGHIJKLMNOPQRSTUVWXYYYYYYYYYYYYYYYYYYYYY" }, secret, {
jwtid: jti,
subject: "GABCDEFGHIJKLMNOPQRSTUVWXYYYYYYYYYYYYYYYYYYYYY",
expiresIn: 60, // 1 minute
issuer: "lance-marketplace",
audience: "lance-frontend",
});

// Build fake req/res/next
const req: any = { headers: { authorization: `Bearer ${token}` }, cookies: {} };
let statusSet: number | null = null;
let jsonBody: any = null;
const res: any = {
status(code: number) { statusSet = code; return this; },
json(body: any) { jsonBody = body; return this; }
};
let nextCalled = false;
const next = () => { nextCalled = true; };

await authGuardModule.authGuard(req, res, next);
assert.strictEqual(statusSet, 401, "authGuard should respond 401 for blacklisted token");
assert(jsonBody && jsonBody.error && jsonBody.error.includes("revoked" || "revoked"), "Expected revoked message");

console.log("ALL TESTS PASSED");
}

main().catch((err) => {
console.error("Tests failed:", err);
process.exit(1);
});
15 changes: 15 additions & 0 deletions contracts/job_registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ pub struct JobAssignedIndexEvent {
pub final_amount: i128,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BidPlacedIndexEvent {
pub job_id: u64,
pub bidder: Address,
pub amount: i128,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct JobStorageReclaimedEvent {
pub job_id: u64,
pub reclaimer: Address,
}

/* -----------------------------------------------------------------
3. Smart Contract Implementation
----------------------------------------------------------------- */
Expand Down
Loading