Skip to content

Commit 7c0c9f5

Browse files
update devops to run smooth
1 parent fea4f58 commit 7c0c9f5

7 files changed

Lines changed: 153 additions & 28 deletions

File tree

‎api/Dockerfile‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,32 @@ RUN npm prune --production && \
5555
# ==============================================================================
5656
FROM node:20-alpine AS production
5757

58+
# ==============================================================================
59+
# Build Arguments (can be passed during docker build)
60+
# ==============================================================================
61+
ARG API_INTERNAL_PORT=3000
62+
ARG PRISMA_STUDIO_PORT=9081
63+
ARG API_MODE=development
64+
ARG MODE=api/dev/v1
65+
ARG API_VERSION=1.0.0
66+
5867
LABEL maintainer="mrdas" \
5968
version="1.0" \
6069
description="Node.js Backend API - Production Image" \
6170
org.opencontainers.image.title="nodejs-backend-with-postgresql" \
6271
org.opencontainers.image.description="Production-ready Node.js/Express backend API" \
6372
org.opencontainers.image.version="1.0.0"
6473

74+
# ==============================================================================
75+
# Environment Variables (runtime, can be overridden by docker-compose)
76+
# ==============================================================================
6577
ENV NODE_ENV=production \
66-
PATH="/api/node_modules/.bin:$PATH"
78+
PATH="/api/node_modules/.bin:$PATH" \
79+
API_INTERNAL_PORT=${API_INTERNAL_PORT} \
80+
PRISMA_STUDIO_PORT=${PRISMA_STUDIO_PORT} \
81+
API_MODE=${API_MODE} \
82+
MODE=${MODE} \
83+
API_VERSION=${API_VERSION}
6784

6885
RUN apk add --no-cache \
6986
curl \
@@ -120,11 +137,15 @@ RUN chmod +x /api/start.sh && \
120137

121138
USER mrdas
122139

123-
EXPOSE 3000 51212
140+
# Expose ports dynamically from build arguments
141+
# These can be overridden via docker-compose build args or --build-arg
142+
EXPOSE ${API_INTERNAL_PORT} ${PRISMA_STUDIO_PORT}
143+
124144

125-
# Health check
145+
# Health check (uses ENV variable set from ARG)
146+
# Using shell form - $API_INTERNAL_PORT expands from ENV at runtime
126147
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
127-
CMD curl -f http://localhost:3000/health || exit 1
148+
CMD sh -c "curl -f http://localhost:\${API_INTERNAL_PORT}/health || exit 1"
128149

129150
ENTRYPOINT ["dumb-init", "--"]
130151
CMD ["/api/start.sh"]

‎api/prisma/seed.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
const { execSync } = require('child_process');
77
const path = require('path');
88
const logger = require('../src/logger/logger');
9+
const { prisma } = require('../src/db/prisma');
910

1011
/**
1112
* Pull schema from remote database
@@ -83,9 +84,14 @@ async function seed() {
8384
const { seed: seedDefaults } = require('./seed-defaults');
8485
await seedDefaults();
8586

87+
// Disconnect Prisma after seeding to prevent hanging
88+
await prisma.$disconnect();
89+
8690
logger.info('Database seeded successfully');
8791
} catch (error) {
8892
logger.error('Failed to seed database', { error: error.message });
93+
// Disconnect even on error
94+
await prisma.$disconnect().catch(() => {});
8995
throw error;
9096
}
9197
}

‎api/router/health/api.js‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,25 @@ const { captureException, captureMessage } = require('../../src/sentry/sentry');
4343
*/
4444
router.get('/health', async (req, res) => {
4545
try {
46+
47+
const baseUrl = `${process.env.API_HOST || 'localhost'}:${process.env.PROXY_PORT || 8900}`;
48+
const mode = process.env.MODE || 'api/dev/v1';
49+
4650
const envInfo = {
4751
API_VERSION: process.env.API_VERSION || 'N/A',
4852
API_MODE: process.env.API_MODE || 'N/A',
49-
MODE: process.env.MODE || 'N/A',
53+
API_URL: baseUrl,
54+
MODE: mode,
5055
UTC: process.env.UTC || 'N/A',
5156
DEBUG_MODE: process.env.DEBUG_MODE || 'N/A',
5257
TIMEZONE: process.env.TIMEZONE || 'N/A',
53-
LOG_LEVEL: process.env.LOG_LEVEL || 'N/A'
58+
LOG_LEVEL: process.env.LOG_LEVEL || 'N/A',
5459
};
5560

5661
const meta = {
5762
service: 'nodejs-backend-with-postgresql-api',
5863
status: 'ok',
59-
env: envInfo
64+
env: envInfo
6065
};
6166

6267
return res.status(200).json(
@@ -115,7 +120,8 @@ router.get('/health', async (req, res) => {
115120
router.get('/health/system', async (req, res) => {
116121
try {
117122
const os = require('os');
118-
123+
const baseUrl = `${process.env.API_HOST || 'localhost'}:${process.env.PROXY_PORT || 8900}`;
124+
const mode = process.env.MODE || 'api/dev/v1';
119125
const systemInfo = {
120126
platform: process.platform,
121127
nodeVersion: process.version,
@@ -129,7 +135,8 @@ router.get('/health/system', async (req, res) => {
129135
const envInfo = {
130136
API_VERSION: process.env.API_VERSION || 'N/A',
131137
API_MODE: process.env.API_MODE || 'N/A',
132-
MODE: process.env.MODE || 'N/A',
138+
API_URL: baseUrl,
139+
MODE: mode,
133140
DEBUG_MODE: process.env.DEBUG_MODE || 'N/A',
134141
LOG_LEVEL: process.env.LOG_LEVEL || 'N/A'
135142
};

‎api/server.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const app = express();
5757
// ============================================================
5858
// ENVIRONMENT VARIABLES
5959
// ============================================================
60+
// Server listens on internal port (3000), PROXY_PORT is for nginx (9080)
6061
const PORT = process.env.API_INTERNAL_PORT || 3000;
6162
const MODE = process.env.MODE || 'api/dev/v1';
6263
const API_MODE = process.env.API_MODE || 'development';
@@ -185,10 +186,14 @@ app.get('/health', async (req, res) => {
185186
db_status = `error: ${dbError.message.substring(0, 50)}`;
186187
}
187188

189+
const baseUrl = `${process.env.API_HOST || 'localhost'}:${process.env.PROXY_PORT || 8900}`;
190+
const mode = process.env.MODE || 'api/dev/v1';
191+
188192
const envInfo = {
189193
API_VERSION: process.env.API_VERSION || 'N/A',
190194
API_MODE: process.env.API_MODE || 'N/A',
191-
MODE: process.env.MODE || 'N/A',
195+
API_URL: baseUrl,
196+
MODE: mode,
192197
UTC: process.env.UTC || 'N/A',
193198
DEBUG_MODE: process.env.DEBUG_MODE || 'N/A',
194199
TIMEZONE: process.env.TIMEZONE || 'N/A',
@@ -199,6 +204,8 @@ app.get('/health', async (req, res) => {
199204
service: 'nodejs-backend-with-postgresql-api',
200205
status: 'ok',
201206
database: db_status,
207+
API_URL: baseUrl,
208+
MODE: mode,
202209
env: envInfo
203210
};
204211

@@ -280,7 +287,6 @@ async function startupEvent() {
280287
// Test database connection (non-blocking)
281288
try {
282289
await prisma.$queryRaw`SELECT 1`;
283-
logger.info('✅ Database connection initialized', { module: 'Server' });
284290
} catch (dbError) {
285291
logger.warn('⚠️ Database connection not available yet (will retry on first use)', {
286292
module: 'Server',

‎api/src/authenticate/otp_cache.js‎

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
const cache = require('../cache/cache');
77
const { randomInt } = require('crypto');
8+
const logger = require('../logger/logger');
89

910
const MASTER_OTP = process.env.MASTER_OTP || process.env.MASTER_ADMIN_OTP || '1408199';
1011

@@ -38,11 +39,35 @@ function generateOtp(length = 6) {
3839
* @returns {Promise<string>} Generated OTP
3940
*/
4041
async function setOtp(userId, ttl = 600) {
42+
// Normalize userId: trim whitespace and lowercase emails
43+
const normalizedUserId = normalizeUserId(userId);
4144
const otp = generateOtp();
42-
await cache.set(`otp:${userId}`, otp, ttl);
45+
const cacheKey = `otp:${normalizedUserId}`;
46+
const success = await cache.set(cacheKey, otp, ttl);
47+
if (!success) {
48+
logger.error('Failed to store OTP in cache', { userId: normalizedUserId, cacheKey });
49+
} else {
50+
logger.info('OTP stored in cache', { userId: normalizedUserId, cacheKey, otpLength: otp.length });
51+
}
4352
return otp;
4453
}
4554

55+
/**
56+
* Normalize user ID for consistent cache key generation
57+
* @param {string} userId - User ID (email or phone)
58+
* @returns {string} Normalized user ID
59+
*/
60+
function normalizeUserId(userId) {
61+
if (!userId) return userId;
62+
// Trim whitespace
63+
let normalized = userId.trim();
64+
// Lowercase emails (emails are case-insensitive)
65+
if (normalized.includes('@')) {
66+
normalized = normalized.toLowerCase();
67+
}
68+
return normalized;
69+
}
70+
4671
/**
4772
* Verify OTP for given user_id
4873
* @param {string} userId - User ID (email or phone)
@@ -51,21 +76,53 @@ async function setOtp(userId, ttl = 600) {
5176
* @returns {Promise<boolean>} True if valid, False otherwise
5277
*/
5378
async function verifyOtp(userId, otp, deleteAfterVerify = true) {
54-
const storedOtp = await cache.get(`otp:${userId}`);
79+
// Normalize userId for consistent cache key lookup
80+
const normalizedUserId = normalizeUserId(userId);
81+
const cacheKey = `otp:${normalizedUserId}`;
82+
83+
// Ensure OTP is a string for comparison
84+
const otpString = String(otp).trim();
5585

5686
// 1. Master OTP check
57-
if (isMasterOtp(otp)) {
87+
if (isMasterOtp(otpString)) {
88+
logger.info('Master OTP verified', { userId: normalizedUserId });
5889
return true;
5990
}
6091

61-
// 2. Normal OTP check
62-
if (storedOtp && storedOtp === otp) {
92+
// 2. Get stored OTP from cache
93+
const storedOtp = await cache.get(cacheKey);
94+
95+
// Log for debugging
96+
if (!storedOtp) {
97+
logger.warn('OTP not found in cache', {
98+
userId: normalizedUserId,
99+
cacheKey,
100+
providedOtp: otpString
101+
});
102+
return false;
103+
}
104+
105+
// Ensure stored OTP is a string for comparison
106+
const storedOtpString = String(storedOtp).trim();
107+
108+
// 3. Normal OTP check (compare as strings)
109+
if (storedOtpString === otpString) {
110+
logger.info('OTP verified successfully', { userId: normalizedUserId });
63111
if (deleteAfterVerify) {
64-
await cache.del(`otp:${userId}`);
112+
await cache.del(cacheKey);
65113
}
66114
return true;
67115
}
68116

117+
logger.warn('OTP mismatch', {
118+
userId: normalizedUserId,
119+
cacheKey,
120+
providedOtp: otpString,
121+
storedOtp: storedOtpString,
122+
storedOtpType: typeof storedOtp,
123+
providedOtpType: typeof otp
124+
});
125+
69126
return false;
70127
}
71128

@@ -77,13 +134,15 @@ async function verifyOtp(userId, otp, deleteAfterVerify = true) {
77134
* @returns {Promise<boolean>} True if valid
78135
*/
79136
async function verifyOtpKeep(userId, otp, deleteAfterVerify = true) {
80-
const stored = await cache.get(`otp:${userId}`);
137+
// Normalize userId for consistent cache key lookup
138+
const normalizedUserId = normalizeUserId(userId);
139+
const stored = await cache.get(`otp:${normalizedUserId}`);
81140
if (!stored || stored !== otp) {
82141
return false;
83142
}
84143

85144
if (deleteAfterVerify) {
86-
await cache.delete(`otp:${userId}`);
145+
await cache.del(`otp:${normalizedUserId}`);
87146
}
88147

89148
return true;
@@ -96,7 +155,9 @@ async function verifyOtpKeep(userId, otp, deleteAfterVerify = true) {
96155
* @returns {Promise<boolean>} True if deleted
97156
*/
98157
async function deleteOtp(userId, otp = null) {
99-
await cache.del(`otp:${userId}`);
158+
// Normalize userId for consistent cache key lookup
159+
const normalizedUserId = normalizeUserId(userId);
160+
await cache.del(`otp:${normalizedUserId}`);
100161
return true;
101162
}
102163

‎api/start.sh‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,30 @@ if [ "$API_MODE" = "development" ]; then
5858
exit 1
5959
}
6060

61-
# if [ -f prisma/seed.js ]; then
62-
# echo "Running Prisma seed script..."
63-
# node prisma/seed.js seed || {
64-
# echo "Warning: Prisma seed failed, continuing..."
65-
# }
66-
# fi
61+
if [ -f prisma/seed.js ]; then
62+
echo "Running Prisma seed script..."
63+
# Run seed with timeout (5 minutes max)
64+
timeout 120 node prisma/seed.js seed || {
65+
SEED_EXIT_CODE=$?
66+
if [ $SEED_EXIT_CODE -eq 124 ]; then
67+
echo "Warning: Prisma seed timed out after 5 minutes, continuing..."
68+
else
69+
echo "Warning: Prisma seed failed (exit code: $SEED_EXIT_CODE), continuing..."
70+
fi
71+
}
72+
fi
73+
74+
# ------------------------------------------------------------------------------
75+
# Start Prisma Studio (Database Dashboard) in development mode
76+
# ------------------------------------------------------------------------------
77+
echo "Starting Prisma Studio (Database Dashboard)..."
78+
# Use PRISMA_STUDIO_PORT env var (set from Dockerfile/docker-compose) with default 51212
79+
# This is the container port where Prisma Studio will run
80+
PRISMA_STUDIO_CONTAINER_PORT=${PRISMA_STUDIO_PORT:-51212}
81+
npx prisma studio --port ${PRISMA_STUDIO_CONTAINER_PORT} --browser none > /dev/null 2>&1 &
82+
PRISMA_STUDIO_PID=$!
83+
echo "✅ Prisma Studio started on port ${PRISMA_STUDIO_CONTAINER_PORT} (PID: $PRISMA_STUDIO_PID)"
84+
echo " Access Prisma Studio at: http://localhost:${PRISMA_STUDIO_PORT:-8901} (from host machine)"
6785
else
6886
echo "Skipping Prisma steps (not dev mode)"
6987
fi

‎docker-compose.yaml‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@ services:
33
build:
44
context: ./api
55
dockerfile: Dockerfile
6+
args:
7+
API_INTERNAL_PORT: ${API_INTERNAL_PORT:-3000}
8+
PRISMA_STUDIO_PORT: ${PRISMA_STUDIO_PORT:-9081}
9+
API_MODE: ${API_MODE:-development}
10+
MODE: ${MODE:-api/dev/v1}
11+
API_VERSION: ${API_VERSION:-1.0.0}
612
image: mrdasdeveloper/nodejs-backend-with-postgresql:base
713
container_name: nodejs-backend
814
restart: unless-stopped
915
env_file: .env
1016
# volumes:
1117
# - ./api:/api
1218
ports:
13-
- "${API_DEBUG_PORT:-8900}:3000"
14-
- "${PRISMA_STUDIO_PORT:-8901}:51212"
19+
- "${API_DEBUG_PORT:-3000}:3000"
20+
- "${PRISMA_STUDIO_PORT:-9081}:${PRISMA_STUDIO_PORT:-9081}"
1521
networks:
1622
- nodejs_backend_with_postgresql_network
1723
depends_on:

0 commit comments

Comments
 (0)