Skip to content

Commit 16f594d

Browse files
authored
Merge pull request #6 from systeminit/push-llwvquzmlyvu
deploy secrets
2 parents c578069 + 5297f3f commit 16f594d

File tree

10 files changed

+1522
-153
lines changed

10 files changed

+1522
-153
lines changed

package-lock.json

Lines changed: 1390 additions & 142 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"license": "Apache-2.0",
1616
"description": "API for Tony's World of Chips",
1717
"dependencies": {
18+
"@aws-sdk/client-secrets-manager": "^3.907.0",
1819
"@prisma/client": "^6.17.0",
1920
"cors": "^2.8.5",
2021
"dotenv": "^17.2.3",

packages/api/prisma/dev.db

0 Bytes
Binary file not shown.

packages/api/src/__tests__/cart.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import express from 'express';
33
import cors from 'cors';
44
import cartRoutes from '../routes/cart';
55
import { errorHandler } from '../middleware/errorHandler';
6-
import prisma from '../config/database';
6+
import { prisma } from './setup';
77

88
// Create test app
99
const app = express();

packages/api/src/__tests__/orders.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import cors from 'cors';
44
import orderRoutes from '../routes/orders';
55
import cartRoutes from '../routes/cart';
66
import { errorHandler } from '../middleware/errorHandler';
7-
import prisma from '../config/database';
7+
import { prisma } from './setup';
88

99
// Create test app with both cart and order routes
1010
const app = express();

packages/api/src/__tests__/products.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import express from 'express';
33
import cors from 'cors';
44
import productRoutes from '../routes/products';
55
import { errorHandler } from '../middleware/errorHandler';
6-
import prisma from '../config/database';
6+
import { prisma } from './setup';
77

88
// Create test app
99
const app = express();

packages/api/src/__tests__/setup.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ beforeEach(async () => {
2121
await prisma.cartItem.deleteMany();
2222
await prisma.order.deleteMany();
2323
});
24+
25+
// Export for use in tests
26+
export { prisma };
Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,50 @@
11
import { PrismaClient } from '@prisma/client';
2+
import { buildDatabaseUrl } from './secrets';
23

3-
const prisma = new PrismaClient({
4-
log: process.env.NODE_ENV === 'test' ? ['error'] : ['warn', 'error'],
5-
});
4+
let prismaInstance: PrismaClient | null = null;
65

7-
export default prisma;
6+
export async function getPrismaClient(): Promise<PrismaClient> {
7+
if (prismaInstance) {
8+
return prismaInstance;
9+
}
10+
11+
// If DATABASE_URL is already set (local dev, tests), use it directly
12+
if (process.env.DATABASE_URL) {
13+
prismaInstance = new PrismaClient({
14+
log: process.env.NODE_ENV === 'test' ? ['error'] : ['warn', 'error'],
15+
});
16+
return prismaInstance;
17+
}
18+
19+
// In production, build DATABASE_URL from secrets
20+
const databaseUrl = await buildDatabaseUrl();
21+
process.env.DATABASE_URL = databaseUrl;
22+
23+
prismaInstance = new PrismaClient({
24+
log: process.env.NODE_ENV === 'test' ? ['error'] : ['warn', 'error'],
25+
});
26+
27+
return prismaInstance;
28+
}
29+
30+
// For synchronous access (local dev and tests where DATABASE_URL is set)
31+
// In production with Secrets Manager, use getPrismaClient() instead
32+
function createPrismaClient(): PrismaClient {
33+
if (prismaInstance) {
34+
return prismaInstance;
35+
}
36+
37+
if (!process.env.DATABASE_URL) {
38+
throw new Error(
39+
'DATABASE_URL is not set. For production with AWS Secrets Manager, initialize with getPrismaClient() first.'
40+
);
41+
}
42+
43+
prismaInstance = new PrismaClient({
44+
log: process.env.NODE_ENV === 'test' ? ['error'] : ['warn', 'error'],
45+
});
46+
47+
return prismaInstance;
48+
}
49+
50+
export default createPrismaClient();

packages/api/src/config/secrets.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
2+
3+
interface DatabaseSecret {
4+
password: string;
5+
username?: string;
6+
engine?: string;
7+
host?: string;
8+
port?: number;
9+
dbname?: string;
10+
}
11+
12+
let cachedSecret: string | null = null;
13+
14+
export async function getDatabasePassword(): Promise<string> {
15+
// If we already fetched the secret, return it
16+
if (cachedSecret) {
17+
return cachedSecret;
18+
}
19+
20+
const secretArn = process.env.DB_SECRET_ARN;
21+
22+
if (!secretArn) {
23+
throw new Error('DB_SECRET_ARN environment variable not set');
24+
}
25+
26+
const client = new SecretsManagerClient({ region: process.env.AWS_REGION || 'us-west-1' });
27+
28+
try {
29+
const command = new GetSecretValueCommand({ SecretId: secretArn });
30+
const response = await client.send(command);
31+
32+
if (!response.SecretString) {
33+
throw new Error('Secret value is empty');
34+
}
35+
36+
const secret: DatabaseSecret = JSON.parse(response.SecretString);
37+
cachedSecret = secret.password;
38+
39+
return cachedSecret;
40+
} catch (error) {
41+
console.error('Error fetching database password from Secrets Manager:', error);
42+
throw error;
43+
}
44+
}
45+
46+
export async function buildDatabaseUrl(): Promise<string> {
47+
const user = process.env.DB_USER || 'postgres';
48+
const host = process.env.DB_HOST;
49+
const port = process.env.DB_PORT || '5432';
50+
const database = process.env.DB_NAME || 'postgres';
51+
52+
if (!host) {
53+
throw new Error('DB_HOST environment variable not set');
54+
}
55+
56+
const password = await getDatabasePassword();
57+
58+
return `postgresql://${user}:${password}@${host}:${port}/${database}?schema=public`;
59+
}

packages/api/src/index.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { errorHandler } from './middleware/errorHandler';
55
import productRoutes from './routes/products';
66
import cartRoutes from './routes/cart';
77
import orderRoutes from './routes/orders';
8+
import { getPrismaClient } from './config/database';
89

910
dotenv.config();
1011

@@ -28,7 +29,21 @@ app.use('/api/orders', orderRoutes);
2829
// Error handler (must be last)
2930
app.use(errorHandler);
3031

31-
// Start server
32-
app.listen(PORT, () => {
33-
console.log(`Server listening on port ${PORT}`);
34-
});
32+
// Initialize database and start server
33+
async function start() {
34+
try {
35+
// Initialize Prisma client (this will fetch secrets if needed)
36+
await getPrismaClient();
37+
console.log('Database connection initialized');
38+
39+
// Start server
40+
app.listen(PORT, () => {
41+
console.log(`Server listening on port ${PORT}`);
42+
});
43+
} catch (error) {
44+
console.error('Failed to start server:', error);
45+
process.exit(1);
46+
}
47+
}
48+
49+
start();

0 commit comments

Comments
 (0)