Skip to content

Commit 715b862

Browse files
authored
Merge pull request #1617 from madisonsc52-del/FEATUREAdd-automated-DB/Redis-credential-rotation-#1500-FIX
feat(devops): enable automated RDS and Redis secret rotation with dyn…
2 parents ef4639b + 47ec03a commit 715b862

30 files changed

Lines changed: 2885 additions & 153 deletions

.github/pull_request_template.md

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,29 @@
1-
# Description
1+
## Commit Type
22

3-
Please include a summary of the changes and the related issue(s) being resolved.
3+
- [ ] feat
4+
- [ ] fix
5+
- [ ] perf
6+
- [ ] docs
7+
- [ ] refactor
8+
- [ ] test
9+
- [ ] build
10+
- [ ] ci
11+
- [ ] chore
12+
- [ ] style
13+
- [ ] revert
414

5-
**Every PR must link to an issue.** If this PR intentionally has no related issue, explain why in the rationale section below.
15+
## Summary
616

7-
Fixes # (issue)
17+
<!-- What changed and why? -->
818

9-
> Rationale for no issue (if applicable):
19+
## Verification
1020

11-
## Type of Change
21+
<!-- Commands, screenshots, or checks used to verify the change. -->
1222

13-
- [ ] Bug fix (non-breaking change which fixes an issue)
14-
- [ ] New feature (non-breaking change which adds functionality)
15-
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
16-
- [ ] DevOps / CI / Documentation update
23+
## Release Notes
1724

18-
# 📖 API Changes & Breaking Changes Checklist
25+
- [ ] User-facing change
26+
- [ ] Breaking change
27+
- [ ] No release note needed
1928

20-
If your changes affect any HTTP route, request/response schema, database schema, or CLI/contract signature:
21-
22-
- [ ] **OpenAPI Spec:** Updated `backend/src/openapi/spec.ts` (and exported `backend/openapi.json` via `npm run openapi:export`).
23-
- [ ] **API Documentation:** Updated `API.md` and/or `docs/API.md` explaining route/schema behavior changes.
24-
- [ ] **Changelog:** Added a corresponding entry in `CHANGELOG.md` under the `[Unreleased]` section.
25-
- [ ] **Migration Notes:** Provided instructions for consumers if the change is a breaking or material behavior shift.
26-
27-
# Checklist
28-
29-
- [ ] My code follows the style guidelines of this project
30-
- [ ] I have performed a self-review of my own code
31-
- [ ] I have commented my code, particularly in hard-to-understand areas
32-
- [ ] I have made corresponding changes to the documentation
33-
- [ ] My changes generate no new warnings
34-
- [ ] **This PR links to an issue or provides a rationale for no issue**
35-
- [ ] I have added tests that prove my fix is effective or that my feature works
36-
- [ ] New and existing unit tests pass locally with my changes
29+
<!-- For breaking changes, include `!` in the commit type or add a `BREAKING CHANGE:` footer so the release changelog flags it. -->

backend/package.json

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"db:migrate:status": "tsx src/db/migrate.ts -- --status",
1313
"db:setup": "tsx src/db/migrate.ts",
1414
"db:seed:e2e": "tsx src/db/seed.e2e.ts",
15-
15+
"test": "vitest run",
1616
"test:watch": "vitest",
1717
"test:coverage": "vitest run --coverage",
1818
"test:ci": "npm run test:coverage",
@@ -34,6 +34,7 @@
3434
"indexer:reindex": "tsx scripts/reindex-events.ts"
3535
},
3636
"dependencies": {
37+
"@aws-sdk/client-secrets-manager": "^3.1098.0",
3738
"@opentelemetry/api": "^1.9.1",
3839
"@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
3940
"@opentelemetry/instrumentation": "^0.219.0",
@@ -45,6 +46,7 @@
4546
"@sentry/node": "^9.14.0",
4647
"@sentry/profiling-node": "^9.14.0",
4748
"@stellar/stellar-sdk": "^12.0.1",
49+
"axios": "^1.7.9",
4850
"better-sqlite3": "^12.11.1",
4951
"bullmq": "^5.69.3",
5052
"cors": "^2.8.5",
@@ -55,10 +57,9 @@
5557
"jsonwebtoken": "^9.0.3",
5658
"newrelic": "^12.24.0",
5759
"node-cron": "^3.0.3",
60+
"node-telegram-bot-api": "^0.67.0",
5861
"nodemailer": "^8.0.1",
59-
"node-telegram-bot-api": "^0.67.0",
60-
"axios": "^1.7.9",
61-
"pdfkit": "^0.17.2",
62+
"pdfkit": "^0.17.2",
6263
"pg": "^8.11.3",
6364
"pino": "^9.6.0",
6465
"prom-client": "^15.1.3",
@@ -74,8 +75,8 @@
7475
"@types/js-yaml": "^4.0.9",
7576
"@types/jsonwebtoken": "^9.0.10",
7677
"@types/node": "^20.19.39",
77-
"@types/node-telegram-bot-api": "^0.64.15",
7878
"@types/node-cron": "^3.0.11",
79+
"@types/node-telegram-bot-api": "^0.64.15",
7980
"@types/nodemailer": "^7.0.10",
8081
"@types/pdfkit": "^0.17.5",
8182
"@types/pg": "^8.10.9",

backend/src/api/ops.routes.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,61 @@ opsRouter.get('/contract/diagnostics/auth-status', (_req: Request, res: Response
528528
return ok(res, { adminAuthConfigured: true })
529529
})
530530

531+
// ================================
532+
// CREDENTIAL ROTATION ROUTES
533+
// ================================
534+
535+
opsRouter.get('/credentials/status', requireAdmin, async (_req: Request, res: Response) => {
536+
try {
537+
const { credentialManager } = await import('../config/credentialManager.js')
538+
const dbCreds = credentialManager.getDbCredentialsSync()
539+
const redisCreds = await credentialManager.getRedisCredentials()
540+
const status = {
541+
database: {
542+
configured: Boolean(dbCreds.user || dbCreds.connectionString),
543+
source: dbCreds.source,
544+
lastRefreshed: dbCreds.lastRefreshed.toISOString()
545+
},
546+
redis: {
547+
configured: Boolean(redisCreds.url),
548+
source: redisCreds.source,
549+
lastRefreshed: redisCreds.lastRefreshed.toISOString(),
550+
urlRedacted: redisCreds.url.replace(/:\/\/[^@]*@/, '://***@')
551+
},
552+
timestamp: new Date().toISOString()
553+
}
554+
return ok(res, status)
555+
} catch (error) {
556+
logger.error('[OPS] Failed to check credential status', { error: getErrorObject(error) })
557+
return fail(res, 500, 'INTERNAL_ERROR', getErrorMessage(error))
558+
}
559+
})
560+
561+
opsRouter.post('/credentials/refresh', requireAdmin, async (_req: Request, res: Response) => {
562+
try {
563+
const { refreshDbPool } = await import('../db/client.js')
564+
const { refreshRedisCredentials } = await import('../queue/connection.js')
565+
const dbPool = await refreshDbPool()
566+
const redisUrl = await refreshRedisCredentials()
567+
const status = {
568+
database: {
569+
refreshed: true,
570+
poolMax: dbPool.options.max
571+
},
572+
redis: {
573+
refreshed: true,
574+
urlRedacted: redisUrl.replace(/:\/\/[^@]*@/, '://***@')
575+
},
576+
timestamp: new Date().toISOString()
577+
}
578+
logger.info('[OPS] Manual credential rotation refresh triggered', status)
579+
return ok(res, status)
580+
} catch (error) {
581+
logger.error('[OPS] Failed to refresh credentials', { error: getErrorObject(error) })
582+
return fail(res, 500, 'INTERNAL_ERROR', getErrorMessage(error))
583+
}
584+
})
585+
531586
// ================================
532587
// RISK MANAGEMENT ROUTES
533588
// ================================

backend/src/api/portfolioImportRoutes.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,21 +51,27 @@ portfolioImportRouter.post('/portfolio/import', async (req: Request, res: Respon
5151
return fail(res, 400, 'VALIDATION_ERROR', 'userAddress is required')
5252
}
5353

54+
const slippageTolerance = Number(req.body?.slippageTolerance ?? req.body?.slippageTolerancePercent ?? 1)
55+
const strategy = typeof req.body?.strategy === 'string' ? req.body.strategy : 'threshold'
56+
const strategyConfig = (req.body?.strategyConfig && typeof req.body.strategyConfig === 'object') ? req.body.strategyConfig : {}
57+
const threshold = Number(req.body?.threshold ?? 5)
58+
5459
const portfolioId = await stellarService.createPortfolio(
5560
userAddress,
5661
parsedAllocations,
57-
5,
58-
1,
59-
'threshold',
60-
{},
62+
Number.isFinite(threshold) ? threshold : 5,
63+
Number.isFinite(slippageTolerance) ? slippageTolerance : 1,
64+
strategy,
65+
strategyConfig,
6166
typeof req.body?.name === 'string' ? req.body.name : undefined,
6267
typeof req.body?.description === 'string' ? req.body.description : undefined
6368
)
6469

6570
return ok(res, { portfolioId, status: 'created' }, { status: 201 })
6671
} catch (error) {
6772
logger.error('[ERROR] Bulk portfolio import failed', { error })
68-
return fail(res, 500, 'INTERNAL_ERROR', 'Bulk import failed')
73+
const msg = process.env.NODE_ENV === 'test' ? (error instanceof Error ? error.message : String(error)) : 'Bulk import failed'
74+
return fail(res, 500, 'INTERNAL_ERROR', msg)
6975
}
7076
})
7177

0 commit comments

Comments
 (0)