backend: add static serving + multi-stage Dockerfile, website: add API test dashboard
This commit is contained in:
19
backend/.env
19
backend/.env
@@ -1,13 +1,14 @@
|
||||
PORT=8080
|
||||
NODE_ENV=development
|
||||
DATABASE_URL=libsql://db-dev-63adec08f5fd35eadac3-sumit22.aws-ap-south-1.turso.io
|
||||
DATABASE_AUTH_TOKEN=eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3ODI3Mzg5MjcsImlkIjoiMDE5ZjEzODUtMzUwMS03ODA0LWExZTEtY2JlN2E4NDlhM2I2Iiwia2lkIjoiczltT1N1V3ZwX2pib21leUZZNzR3RVNoN1NOak12RmVDc3EtZk5YdHY1NCIsInJpZCI6IjU5OTg0M2RkLTc4NWItNDhhZS04MDI0LTgxODM2YmQzZDE0MSJ9.TsozKx4lkITuiGiQ7FQDR_xl1LiZtzExx6nDkrZgefNXI7QPWiKYrSTgapqNMlO9QiGzE_cPbcClHvCPnmgMDg
|
||||
S3_ENDPOINT=https://t3.storage.dev
|
||||
S3_BUCKET=plena-staging
|
||||
S3_PREFIX=backends/6892099c9a4be35629c49e8c/6a426fea762485cd9a634857/dev/
|
||||
S3_ACCESS_KEY_ID=tid_VozbolUlzrxAAhDfPdanwUzAMqsO_E_EroKpPZnCcwOuXlHUKd
|
||||
S3_SECRET_ACCESS_KEY=tsec_C6sLdJDir0Kz+4abePDcJIdah+zA7dni1Z5nNY3lH5f4-dd+Nr+GohZuXgZwetr1Hv-270
|
||||
S3_SESSION_TOKEN=
|
||||
PROJECT_ID=6a426fea762485cd9a634857
|
||||
API_PREFIX=
|
||||
CORS_ORIGIN=*
|
||||
LOG_LEVEL=debug
|
||||
PROJECT_ID=6a1d3e08d7c7b2a82853a3dd
|
||||
DATABASE_URL=libsql://db-17b1345995727386ada9-sumit22.aws-ap-south-1.turso.io
|
||||
DATABASE_AUTH_TOKEN=eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJhIjoicnciLCJpYXQiOjE3ODIxOTUxMDksImlkIjoiMDE5ZWVlZDctNTAwMS03MjkwLWE4YmEtY2FlMjMwMDkzOTcxIiwicmlkIjoiOGVlNjZiZmItNmI5NS00MjdiLTlhYjMtZDljODRkMTVlZDljIn0.8QLPZHplF5ofeifQz9vrAeVp3R0mN_2bELWH4WsSvRovsz4T6jr2c1J6fTStBXOvRTkatwQ7FOdpVWNfFGI4Ag
|
||||
S3_ENDPOINT=https://t3.storage.dev
|
||||
S3_BUCKET=plena-staging
|
||||
S3_PREFIX=backends/template-project/
|
||||
S3_ACCESS_KEY_ID=tid_bBVTJLLQFGYJFbjMnPYjKvPtYZRzVXGFMrATpuMvySWEkgCEME
|
||||
S3_SECRET_ACCESS_KEY=tsec_7R_ZSENyKt_7GiIyse2Y4ZgZDwsqzOWajWKrg6CJrfiHOwVPIExYMp0PtVUpEUV7hmCPip
|
||||
S3_SESSION_TOKEN=
|
||||
|
||||
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
.env
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
FROM oven/bun:1.2.18 AS base
|
||||
FROM oven/bun:1.2.18 AS website-builder
|
||||
WORKDIR /website
|
||||
COPY website/package.json ./
|
||||
RUN bun install
|
||||
COPY website/ .
|
||||
RUN bun run build
|
||||
|
||||
FROM oven/bun:1.2.18
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock tsconfig.json ./
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
COPY src ./src
|
||||
COPY backend/package.json backend/bun.lock backend/tsconfig.json ./
|
||||
RUN bun install --frozen-lockfile --production
|
||||
COPY backend/src ./src
|
||||
COPY --from=website-builder /website/dist ./public
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=8080
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.0",
|
||||
"express": "^5.1.0",
|
||||
"node-cron": "^4.5.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
@@ -61,7 +60,7 @@
|
||||
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
@@ -127,8 +126,6 @@
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"node-cron": ["node-cron@4.5.0", "", {}, "sha512-4Trh+kjvbXokyJkwQumvD5YAgeJfgHLR/sKyu71uSmxfCR5QMO1hldpvmFZOICN5pLgNY+J5Y8+ar3XKo5/4tQ=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.0",
|
||||
"express": "^5.1.0",
|
||||
"node-cron": "^4.5.0"
|
||||
"express": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
|
||||
23
backend/public/index.html
Normal file
23
backend/public/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Backend Running</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; color: #333; }
|
||||
.card { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); text-align: center; }
|
||||
h1 { margin: 0 0 0.5rem; font-size: 1.5rem; }
|
||||
p { margin: 0.25rem 0; color: #666; }
|
||||
a { color: #0066cc; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Backend Running</h1>
|
||||
<p>API <a href="/api/health">/api/health</a></p>
|
||||
<p>Frontend build not found — run <code>bun run build</code> in website/</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,12 +1,15 @@
|
||||
import 'dotenv/config';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import cors from 'cors';
|
||||
import express, { type NextFunction, type Request, type Response } from 'express';
|
||||
import cron from 'node-cron';
|
||||
import { buildApiPath, getConfig } from './config';
|
||||
import { TursoClient } from './lib/db';
|
||||
import { createHealthRouter } from './routes/health';
|
||||
import { createTodosRouter } from './routes/todos';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const config = getConfig();
|
||||
const app = express();
|
||||
const db = new TursoClient(config);
|
||||
@@ -19,24 +22,14 @@ const apiRoot = config.apiPrefix || '/';
|
||||
app.use(apiRoot, createHealthRouter(config));
|
||||
app.use(apiRoot, createTodosRouter(config, db));
|
||||
|
||||
// 1. Cron Job A: Runs every 3 minutes
|
||||
cron.schedule('*/3 * * * *', () => {
|
||||
console.log('[Cron A] Heartbeat executing every 3 minutes: ' + new Date().toISOString());
|
||||
});
|
||||
const publicDir = path.join(__dirname, '../public');
|
||||
app.use(express.static(publicDir));
|
||||
|
||||
// 2. Cron Job B: Runs every 10 minutes
|
||||
cron.schedule('*/10 * * * *', () => {
|
||||
console.log('[Cron B] Heartbeat executing every 10 minutes: ' + new Date().toISOString());
|
||||
});
|
||||
|
||||
// 3. Cloud Run Cron Tick endpoint (No-op to wake up event loop)
|
||||
app.post('/_cron/tick', (req, res) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || authHeader !== 'Bearer ' + process.env.CRON_TOKEN) {
|
||||
return res.status(401).json({ success: false, error: 'Unauthorized' });
|
||||
}
|
||||
console.log('[Tick] Heartbeat received. Waking up event loop.');
|
||||
res.status(200).json({ success: true });
|
||||
app.get('*', (req, res, next) => {
|
||||
if (req.path.startsWith(apiRoot)) return next();
|
||||
res.sendFile(path.join(publicDir, 'index.html'), (err) => {
|
||||
if (err) next();
|
||||
});
|
||||
});
|
||||
|
||||
app.use((_req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user