Skip to content

Commit 319d03c

Browse files
authored
Merge pull request #293 from JTKaduma/feat/secret-management
feat: add dev env and more
2 parents fda57d1 + e94fe94 commit 319d03c

35 files changed

Lines changed: 2068 additions & 416 deletions

backend/.env.example

Lines changed: 298 additions & 69 deletions
Large diffs are not rendered by default.

backend/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,14 @@ GraphQL is exposed at `/api/graphql`.
7171
```bash
7272
cd backend
7373
npm i
74+
npm run env:example:generate
7475
npm run start:dev
7576
```
7677

78+
Environment configuration is defined in [`src/config/env.definitions.ts`](/home/json/Desktop/Drips/niff-Stellar-shurance/backend/src/config/env.definitions.ts). Update that file first, then regenerate [`backend/.env.example`](/home/json/Desktop/Drips/niff-Stellar-shurance/backend/.env.example) with `npm run env:example:generate`.
79+
80+
Secrets guidance and rotation procedures live in [`docs/ops/secrets-management-runbook.md`](/home/json/Desktop/Drips/niff-Stellar-shurance/docs/ops/secrets-management-runbook.md). Generate a fresh JWT signing key with `npm run secrets:generate:jwt`.
81+
7782
## Deployment
7883
Docker: `make docker-up`
7984

backend/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131
"seed": "ts-node src/database/seed.ts",
3232
"db:reset": "docker-compose down -v && docker-compose up -d",
3333
"error-catalog:check": "ts-node scripts/check-error-codes.ts",
34-
"error-catalog:export": "ts-node scripts/export-error-catalog.ts"
34+
"error-catalog:export": "ts-node scripts/export-error-catalog.ts",
35+
"env:example:generate": "ts-node scripts/generate-env-example.ts",
36+
"env:example:check": "ts-node scripts/check-env-example.ts",
37+
"secrets:generate:jwt": "ts-node scripts/generate-jwt-key.ts"
3538
},
3639
"dependencies": {
3740
"@apollo/server": "^4.13.0",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { readFileSync } from 'fs';
2+
import { join } from 'path';
3+
import { renderEnvExample } from '../src/config/env.definitions';
4+
5+
const envExamplePath = join(__dirname, '..', '.env.example');
6+
const expected = renderEnvExample();
7+
const actual = readFileSync(envExamplePath, 'utf8');
8+
9+
if (actual !== expected) {
10+
console.error('.env.example is out of date. Run `npm run env:example:generate` in backend/.');
11+
process.exit(1);
12+
}
13+
14+
console.log('.env.example is up to date.');
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { writeFileSync } from 'fs';
2+
import { join } from 'path';
3+
import { renderEnvExample } from '../src/config/env.definitions';
4+
5+
const outputPath = join(__dirname, '..', '.env.example');
6+
writeFileSync(outputPath, renderEnvExample(), 'utf8');
7+
console.log(`Wrote ${outputPath}`);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { randomBytes } from 'crypto';
2+
import { chmodSync, writeFileSync } from 'fs';
3+
import { resolve } from 'path';
4+
5+
const args = process.argv.slice(2);
6+
const outputFlagIndex = args.indexOf('--output');
7+
const outputPath =
8+
outputFlagIndex >= 0 && args[outputFlagIndex + 1]
9+
? resolve(args[outputFlagIndex + 1])
10+
: null;
11+
12+
const key = randomBytes(64).toString('base64url');
13+
const timestamp = new Date().toISOString();
14+
const payload = [
15+
'# Generated JWT signing key',
16+
`# Generated at: ${timestamp}`,
17+
'# Store this value in your secrets manager, not in git.',
18+
`JWT_SECRET=${key}`,
19+
'',
20+
].join('\n');
21+
22+
if (outputPath) {
23+
writeFileSync(outputPath, payload, { encoding: 'utf8', mode: 0o600 });
24+
chmodSync(outputPath, 0o600);
25+
console.log(`Wrote JWT key material to ${outputPath} with mode 600`);
26+
} else {
27+
process.stdout.write(payload);
28+
}

backend/src/__tests__/observability/redaction.spec.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { redactHeaders, redactBody } from '../../common/logger/app-logger.service';
1+
import {
2+
redactHeaders,
3+
redactBody,
4+
redactMessageText,
5+
redactValue,
6+
} from '../../common/logger/app-logger.service';
27

38
describe('redactHeaders', () => {
49
it('redacts Authorization header', () => {
@@ -53,3 +58,34 @@ describe('redactBody', () => {
5358
expect(redactBody(undefined)).toBeUndefined();
5459
});
5560
});
61+
62+
describe('redactValue', () => {
63+
it('redacts nested secret-like fields', () => {
64+
const redacted = redactValue({
65+
jwtSecret: 'super-secret-value',
66+
nested: {
67+
apiKey: 'another-secret',
68+
},
69+
});
70+
71+
expect(redacted).toEqual({
72+
jwtSecret: '[REDACTED]',
73+
nested: {
74+
apiKey: '[REDACTED]',
75+
},
76+
});
77+
});
78+
});
79+
80+
describe('redactMessageText', () => {
81+
it('redacts secret assignments and bearer tokens inside log messages', () => {
82+
const redacted = redactMessageText(
83+
'JWT_SECRET=my-secret-value authorization=Bearer abc.def.ghi',
84+
);
85+
86+
expect(redacted).not.toContain('my-secret-value');
87+
expect(redacted).not.toContain('abc.def.ghi');
88+
expect(redacted).toContain('JWT_SECRET=[REDACTED]');
89+
expect(redacted).toContain('Bearer [REDACTED]');
90+
});
91+
});

backend/src/__tests__/ramp.test.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Test, TestingModule } from '@nestjs/testing';
22
import { NotFoundException } from '@nestjs/common';
3-
import { RampController } from '../../ramp/ramp.controller';
4-
import { FeatureFlagsService } from '../../feature-flags/feature-flags.service';
3+
import { RampController } from '../ramp/ramp.controller';
4+
import { FeatureFlagsService } from '../feature-flags/feature-flags.service';
55
import { Reflector } from '@nestjs/core';
6-
import { FeatureFlagsGuard } from '../../feature-flags/feature-flags.guard';
6+
import { FeatureFlagsGuard } from '../feature-flags/feature-flags.guard';
7+
import { ConfigService } from '@nestjs/config';
78

89
describe('RampController', () => {
910
let controller: RampController;
@@ -19,6 +20,21 @@ describe('RampController', () => {
1920
controllers: [RampController],
2021
providers: [
2122
{ provide: FeatureFlagsService, useValue: mockFlags(flagEnabled) },
23+
{
24+
provide: ConfigService,
25+
useValue: {
26+
get: jest.fn((key: string, fallback?: string) => {
27+
const values: Record<string, string> = {
28+
RAMP_URL: process.env.RAMP_URL ?? '',
29+
RAMP_ALLOWED_REGIONS: process.env.RAMP_ALLOWED_REGIONS ?? '',
30+
RAMP_UTM_SOURCE: 'niffyinsure',
31+
RAMP_UTM_MEDIUM: 'app',
32+
RAMP_UTM_CAMPAIGN: 'onramp',
33+
};
34+
return values[key] ?? fallback;
35+
}),
36+
},
37+
},
2238
Reflector,
2339
FeatureFlagsGuard,
2440
],

backend/src/app.module.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ConfigModule } from '@nestjs/config';
33
import { TerminusModule } from '@nestjs/terminus';
44
import { ThrottlerModule, ThrottlerStorage } from '@nestjs/throttler';
55
import { APP_GUARD } from '@nestjs/core';
6-
import { validationSchema } from './config/env.validation';
6+
import { validateEnvironment } from './config/env.validation';
77
import { HealthModule } from './health/health.module';
88
import { PrismaModule } from './prisma/prisma.module';
99
import { CacheModule } from './cache/cache.module';
@@ -35,9 +35,9 @@ import { IdempotencyMiddleware } from './common/middleware/idempotency.middlewar
3535
ConfigModule.forRoot({
3636
isGlobal: true,
3737
envFilePath: '.env',
38-
validationSchema,
38+
validate: validateEnvironment,
3939
validationOptions: {
40-
abortEarly: true,
40+
abortEarly: false,
4141
},
4242
}),
4343
ThrottlerModule.forRootAsync({

backend/src/auth/auth.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { AuthIdentityService } from './auth-identity.service';
1515
imports: [ConfigModule],
1616
useFactory: (configService: ConfigService) => ({
1717
secret: configService.get<string>('JWT_SECRET'),
18-
signOptions: { expiresIn: '7d' },
18+
signOptions: { expiresIn: configService.get<string>('JWT_EXPIRES_IN', '7d') },
1919
}),
2020
inject: [ConfigService],
2121
}),

0 commit comments

Comments
 (0)