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
22 changes: 22 additions & 0 deletions db/migrations/20260226000000_create_sessions_table.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Migration to create the sessions table for token revocation and session management.
*/
exports.up = async function up(knex) {
await knex.schema.createTable('sessions', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'))
table.string('user_id', 255).notNullable()
table.string('jti', 255).notNullable().unique()
table.timestamp('revoked_at', { useTz: true }).nullable()
table.timestamp('expires_at', { useTz: true }).notNullable()
table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now())
})

await knex.schema.alterTable('sessions', (table) => {
table.index(['user_id'], 'idx_sessions_user_id')
table.index(['jti'], 'idx_sessions_jti')
})
}

exports.down = async function down(knex) {
await knex.schema.dropTableIfExists('sessions')
}
19,846 changes: 5,348 additions & 14,498 deletions package-lock.json

Large diffs are not rendered by default.

69 changes: 12 additions & 57 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,47 +1,4 @@
{
"name": "disciplr-backend",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"lint": "eslint src --ext .ts",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
"test:api-keys": "tsx --test src/routes/apiKeys.test.ts",
"migrate:make": "knex migrate:make --knexfile knexfile.cjs --migrations-directory db/migrations --extension cjs",
"migrate:latest": "knex migrate:latest --knexfile knexfile.cjs",
"migrate:rollback": "knex migrate:rollback --knexfile knexfile.cjs",
"migrate:status": "knex migrate:status --knexfile knexfile.cjs"
},
"dependencies": {
"@stellar/stellar-sdk": "^14.5.0",
"cors": "^2.8.6",
"express": "^4.21.0",
"helmet": "^7.2.0",
"jsonwebtoken": "^9.0.3",
"pg": "^8.13.1"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^4.17.21",
"@types/jest": "^30.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^22.9.0",
"@types/supertest": "^6.0.3",
"eslint": "^9.13.0",
"jest": "^30.2.0",
"knex": "^3.1.0",
"prisma": "^6.19.2",
"supertest": "^7.2.2",
"ts-jest": "^29.4.6",
"tsx": "^4.19.2",
"typescript": "^5.6.3",
"vitest": "^4.0.18"
}
}
"name": "disciplr-backend",
"version": "0.1.0",
"private": true,
Expand All @@ -53,10 +10,6 @@
"lint": "eslint src --ext .ts",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
"test:api-keys": "tsx --test src/routes/apiKeys.test.ts",
"test:api-keys": "tsx --test src/routes/apiKeys.test.ts",
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
"test:api-keys": "node --experimental-vm-modules node_modules/jest/bin/jest.js src/routes/apiKeys.test.ts",
"test:vaults": "tsx --test src/routes/vaults.test.ts",
"migrate:make": "knex migrate:make --knexfile knexfile.cjs --migrations-directory db/migrations --extension cjs",
Expand All @@ -66,30 +19,32 @@
},
"dependencies": {
"@prisma/client": "^6.19.2",
"@stellar/stellar-sdk": "^14.5.0",
"bcryptjs": "^3.0.3",
"better-sqlite3": "^11.5.0",
"cors": "^2.8.5",
"cors": "^2.8.6",
"csv-stringify": "^6.6.0",
"dotenv": "^16.4.7",
"express": "^4.21.0",
"express-rate-limit": "^8.2.1",
"helmet": "^7.1.0",
"express-rate-limit": "^7.5.0",
"helmet": "^7.2.0",
"jsonwebtoken": "^9.0.3",
"zod": "^4.3.6",
"pg": "^8.13.1"
"pg": "^8.13.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
"@typescript-eslint/eslint-plugin": "^8.46.0",
"@typescript-eslint/parser": "^8.46.0",
"@types/bcryptjs": "^2.4.6",
"@types/better-sqlite3": "^7.6.13",
"@types/cors": "^2.8.19",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.13",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^22.9.0",
"@types/pg": "^8.11.11",
"@types/supertest": "^6.0.3",
"dotenv": "^17.3.1",
"@typescript-eslint/eslint-plugin": "^8.14.0",
"@typescript-eslint/parser": "^8.14.0",
"eslint": "^9.13.0",
"jest": "^29.7.0",
"knex": "^3.1.0",
Expand All @@ -98,6 +53,6 @@
"ts-jest": "^29.2.5",
"tsx": "^4.19.2",
"typescript": "^5.6.3",
"vitest": "^4.0.18"
"vitest": "^2.1.4"
}
}
}
70 changes: 17 additions & 53 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,34 @@
import cors from 'cors'
import express from 'express'
import helmet from 'helmet'
import { analyticsRouter } from './routes/analytics.js'
import { apiKeysRouter } from './routes/apiKeys.js'
import { healthRouter } from './routes/health.js'
import { vaultsRouter } from './routes/vaults.js'
import { milestonesRouter } from './routes/milestones.js'
import { orgVaultsRouter } from './routes/orgVaults.js'
import { orgAnalyticsRouter } from './routes/orgAnalytics.js'
import { verificationsRouter } from './routes/verifications.js'
import { adminVerifiersRouter } from './routes/adminVerifiers.js'
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import { vaultsRouter } from './routes/vaults.js';
import { healthRouter } from './routes/health.js';
import { analyticsRouter } from './routes/analytics.js';
import { apiKeysRouter } from './routes/apiKeys.js';
import { transactionsRouter } from './routes/transactions.js';
import { privacyRouter } from './routes/privacy.js';
import { privacyLogger } from './middleware/privacy-logger.js';
import cors from 'cors'
import { privacyLogger } from './middleware/privacy-logger.js'

export const app = express();
export const app = express()

app.use(helmet())
app.use(cors({ origin: true }))
app.use(express.json())
app.use((_req, res, next) => {
res.setHeader('X-Timezone', 'UTC')
next()
})
app.use(helmet());

app.use('/api/health', healthRouter)
app.use('/api/vaults', vaultsRouter)
app.use('/api/vaults/:vaultId/milestones', milestonesRouter)
app.use('/api/analytics', analyticsRouter)
app.use('/api/api-keys', apiKeysRouter)
app.use('/api/organizations', orgVaultsRouter)
app.use('/api/organizations', orgAnalyticsRouter)
app.use('/api/verifications', verificationsRouter)
app.use('/api/admin/verifiers', adminVerifiersRouter)
// 2. CORS: Origin validation
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'];

// CORS Configuration
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']
const corsOptions: cors.CorsOptions = {
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
callback(null, true)
} else {
callback(new Error('Not allowed by CORS'));
callback(new Error('Not allowed by CORS'))
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
};
}

app.use(cors(corsOptions))
app.use(express.json())
app.use(privacyLogger)

app.use(cors(corsOptions));
app.use(express.json());
app.use(privacyLogger);
app.use((_req, res, next) => {
res.setHeader('X-Timezone', 'UTC')
next()
})

// Routes
app.use('/api/health', healthRouter);
app.use('/api/vaults', vaultsRouter);
app.use('/api/analytics', analyticsRouter);
app.use('/api/api-keys', apiKeysRouter);
app.use('/api/transactions', transactionsRouter);
app.use('/api/privacy', privacyRouter);
// Routes are mounted in index.ts
4 changes: 2 additions & 2 deletions src/db/database.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Database from 'better-sqlite3'
import Database, { Database as DatabaseType } from 'better-sqlite3'
import path from 'path'
import { fileURLToPath } from 'url'
import fs from 'fs'
Expand All @@ -13,7 +13,7 @@ if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true })
}

export const db = new Database(dbPath)
export const db: DatabaseType = new Database(dbPath)

// Enable WAL mode for better performance
db.pragma('journal_mode = WAL')
Expand Down
10 changes: 5 additions & 5 deletions src/db/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import knex from 'knex'
// @ts-ignore
import knexConfig from '../../knexfile.cjs'

const db = knex(knexConfig as any)

export { db }
import pg from 'pg';
import 'dotenv/config';

const { Pool } = pg;

// Ensure DATABASE_URL is in your .env file
// Use for migrations and legacy logic
export const db = knex((knexConfig as any).default || knexConfig)

// Use for high-performance direct queries
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
Expand Down
12 changes: 4 additions & 8 deletions src/db/knex.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import knex, { Knex } from 'knex'
import knexConfig from '../../knexfile.cjs'
import { createRequire } from 'module'

const require = createRequire(import.meta.url)
const knexConfig = require('../../knexfile.cjs')

/**
* Knex database connection instance
Expand All @@ -13,10 +16,3 @@ export const db: Knex = knex(knexConfig)
export async function closeDatabase(): Promise<void> {
await db.destroy()
}
import { createRequire } from 'module'
import knex from 'knex'

const require = createRequire(import.meta.url)
const config = require('../../knexfile.cjs')

export const db = knex(config)
70 changes: 19 additions & 51 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import { app } from './app.js'
import express from 'express'
import cors from 'cors'
import helmet from 'helmet'
import { vaultsRouter } from './routes/vaults.js'
import { createHealthRouter } from './routes/health.js'
import { createJobsRouter } from './routes/jobs.js'
import { BackgroundJobSystem } from './jobs/system.js'
import { vaultsRouter, Vault } from './routes/vaults.js'
import { authRouter } from './routes/auth.js'
import { analyticsRouter } from './routes/analytics.js'
import { healthRouter } from './routes/health.js'
import { healthRateLimiter, vaultsRateLimiter } from './middleware/rateLimiter.js'
import { createExportRouter } from './routes/exports.js'
import { apiKeysRouter } from './routes/apiKeys.js'
import { transactionsRouter } from './routes/transactions.js'
import { privacyRouter } from './routes/privacy.js'
import { adminRouter } from './routes/admin.js'
import { adminVerifiersRouter } from './routes/adminVerifiers.js'
import { verificationsRouter } from './routes/verifications.js'
import { milestonesRouter } from './routes/milestones.js'
import { privacyLogger } from './middleware/privacy-logger.js'
import { startExpirationChecker } from './services/expirationScheduler.js'
import { orgVaultsRouter } from './routes/orgVaults.js'
import { orgAnalyticsRouter } from './routes/orgAnalytics.js'
import { privacyLogger } from './middleware/privacy-logger.js'
import { adminRouter } from './routes/admin.js'
import { startExpirationChecker } from './services/expirationScheduler.js'
import { healthRateLimiter, vaultsRateLimiter } from './middleware/rateLimiter.js'
import {
securityMetricsMiddleware,
securityRateLimitMiddleware,
Expand All @@ -31,71 +26,44 @@ const PORT = process.env.PORT ?? 3000
const jobSystem = new BackgroundJobSystem()

jobSystem.start()

// Initialize SQLite database for analytics
initializeDatabase()

app.use(helmet())
app.use(
cors({
origin: '*', // Adjust this to specific origins for better security
}),
)
app.use(express.json())
app.use(securityMetricsMiddleware)
app.use(securityRateLimitMiddleware)
app.use(privacyLogger)

app.use('/api/health', createHealthRouter(jobSystem))
app.use('/api/health', healthRateLimiter, createHealthRouter(jobSystem))
app.use('/api/jobs', createJobsRouter(jobSystem))
app.use('/api/vaults', vaultsRouter)
app.use('/api/vaults/:vaultId/milestones', milestonesRouter)
app.use('/api/health', healthRateLimiter, healthRouter)
app.use('/api/vaults', vaultsRateLimiter, vaultsRouter)
app.use('/api/auth', authRouter)
app.use('/api/exports', createExportRouter(Vault))
app.use('/api/vaults', vaultsRateLimiter, vaultsRouter)
app.use('/api/vaults/:vaultId/milestones', milestonesRouter)
app.use('/api/transactions', transactionsRouter)
app.use('/api/analytics', analyticsRouter)
app.use('/api/api-keys', apiKeysRouter)
app.use('/api/privacy', privacyRouter)
app.use('/api/organizations', orgVaultsRouter)
app.use('/api/organizations', orgAnalyticsRouter)
app.use('/api/admin', adminRouter)
app.use('/api/admin/verifiers', adminVerifiersRouter)
app.use('/api/verifications', verificationsRouter)

const server = app.listen(PORT, () => {
console.log(`Disciplr API listening on http://localhost:${PORT}`)
startExpirationChecker()
})

let shuttingDown = false

const shutdown = async (signal: NodeJS.Signals): Promise<void> => {
if (shuttingDown) {
return
}

shuttingDown = true
const shutdown = async (signal: string) => {
console.log(`Received ${signal}. Shutting down gracefully...`)

try {
await jobSystem.stop()
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error)
return
}
resolve()
})
server.close(() => {
console.log('HTTP server closed.')
process.exit(0)
})
process.exit(0)
} catch (error) {
console.error('Failed during shutdown:', error)
console.error('Error during shutdown:', error)
process.exit(1)
}
}

for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.on(signal, () => {
void shutdown(signal)
})
}
process.on('SIGINT', () => shutdown('SIGINT'))
process.on('SIGTERM', () => shutdown('SIGTERM'))
Loading