diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..c07ae1cc9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,93 @@ +# Dependencies +node_modules/ +**/node_modules/ +.pnpm-store/ + +# Build outputs +dist/ +build/ +**/dist/ +**/build/ +*.tsbuildinfo + +# Development files +.git/ +.gitignore +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Test files +test/ +**/test/ +**/*.spec.ts +**/*.test.ts +**/*.e2e-spec.ts +coverage/ +.nyc_output/ +test-results/ +playwright-report/ + +# Documentation +*.md +!README.md +docs/ +CHANGELOG.md +CONTRIBUTING.md +DISCLAIMER +LICENSE + +# CI/CD +.github/ +.gitpod.yml + +# Environment files +.env +.env.* +!.env.example + +# Logs +*.log +logs/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# OS files +.DS_Store +Thumbs.db +*.pid +*.seed +*.pid.lock + +# Docker files (don't copy Dockerfiles into themselves) +docker-compose*.yml +Dockerfile* +.dockerignore + +# Misc +.cache/ +.temp/ +tmp/ +*.tmp +.qodo/ +e2e/ +playwright.config.ts + +# Source maps (not needed in production) +*.map + +# TypeScript configs (not needed at runtime) +tsconfig*.json +!tsconfig.json + +# Linting/formatting +.eslintrc* +.prettierrc* +.eslintcache + +# Package manager locks (we copy them explicitly) +# pnpm-lock.yaml diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b4f82a6f0..6c638a7fe 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -58,6 +58,12 @@ services: # System database - SYSTEM_DB_NAME=${SYSTEM_DB_NAME} + # Redis + - REDIS_HOST=redis + - REDIS_PORT=6379 + - QUEUE_HOST=redis + - QUEUE_PORT=6379 + # Tenants databases - TENANT_DB_NAME_PERFIX=${TENANT_DB_NAME_PERFIX} diff --git a/docker/migration/Dockerfile b/docker/migration/Dockerfile index d61ef0679..deeb0fefb 100644 --- a/docker/migration/Dockerfile +++ b/docker/migration/Dockerfile @@ -35,4 +35,4 @@ WORKDIR /app/packages/server RUN git clone https://github.com/vishnubob/wait-for-it.git # Once we listen the mysql port run the migration task. -CMD ./wait-for-it/wait-for-it.sh mysql:3306 -- sh -c "node ./build/commands.js system:migrate:latest && node ./build/commands.js tenants:migrate:latest" +CMD ./wait-for-it/wait-for-it.sh mysql:3306 -- sh -c "pnpm run system:migrate:latest && pnpm run tenants:migrate:latest" diff --git a/packages/server/Dockerfile b/packages/server/Dockerfile index 3339a358a..0acd51a23 100644 --- a/packages/server/Dockerfile +++ b/packages/server/Dockerfile @@ -1,100 +1,99 @@ -FROM node:18.16.0-alpine as build +# Stage 1: Build +FROM node:18.16.0-alpine AS builder -USER root - -ARG MAIL_HOST= \ - MAIL_USERNAME= \ - MAIL_PASSWORD= \ - MAIL_PORT= \ - MAIL_SECURE= \ - MAIL_FROM_NAME= \ - MAIL_FROM_ADDRESS= \ - # Database - DB_HOST= \ - DB_USER= \ - DB_PASSWORD= \ - DB_CHARSET= \ - # System database. - SYSTEM_DB_NAME= \ - SYSTEM_DB_PASSWORD= \ - SYSTEM_DB_USER= \ - SYSTEM_DB_HOST= \ - SYSTEM_DB_CHARSET= \ - # Tenant databases. - TENANT_DB_USER= \ - TENANT_DB_PASSWORD= \ - TENANT_DB_HOST= \ - TENANT_DB_NAME_PERFIX= \ - TENANT_DB_CHARSET= \ - # Authentication - JWT_SECRET= \ - # Application - BASE_URL= \ - # Sign-up restriction - SIGNUP_DISABLED= \ - SIGNUP_ALLOWED_DOMAINS= \ - SIGNUP_ALLOWED_EMAILS= - -ENV MAIL_HOST=$MAIL_HOST \ - MAIL_USERNAME=$MAIL_USERNAME \ - MAIL_PASSWORD=$MAIL_PASSWORD \ - MAIL_PORT=$MAIL_PORT \ - MAIL_SECURE=$MAIL_SECURE \ - MAIL_FROM_NAME=$MAIL_FROM_NAME \ - MAIL_FROM_ADDRESS=$MAIL_FROM_ADDRESS \ - # Database - DB_HOST=$DB_HOST \ - DB_USER=$DB_USER \ - DB_PASSWORD=$DB_PASSWORD \ - DB_CHARSET=$DB_CHARSET \ - # System database. - SYSTEM_DB_HOST=$SYSTEM_DB_HOST \ - SYSTEM_DB_USER=$SYSTEM_DB_USER \ - SYSTEM_DB_PASSWORD=$SYSTEM_DB_PASSWORD \ - SYSTEM_DB_NAME=$SYSTEM_DB_NAME \ - SYSTEM_DB_CHARSET=$SYSTEM_DB_CHARSET \ - # Tenant databases. - TENANT_DB_NAME_PERFIX=$TENANT_DB_NAME_PERFIX \ - TENANT_DB_HOST=$TENANT_DB_HOST \ - TENANT_DB_PASSWORD=$TENANT_DB_PASSWORD \ - TENANT_DB_USER=$TENANT_DB_USER \ - TENANT_DB_CHARSET=$TENANT_DB_CHARSET \ - # Authentication - JWT_SECRET=$JWT_SECRET \ - # Application - BASE_URL=$BASE_URL \ - # Sign-up restriction - SIGNUP_DISABLED=$SIGNUP_DISABLED \ - SIGNUP_ALLOWED_DOMAINS=$SIGNUP_ALLOWED_DOMAINS \ - SIGNUP_ALLOWED_EMAILS=$SIGNUP_ALLOWED_EMAILS - -# New Relic config file. -ENV NEW_RELIC_NO_CONFIG_FILE=true - -# Create app directory. WORKDIR /app -RUN chown node:node / +# Install pnpm +RUN npm install -g pnpm@8.10.2 -# Install pnpm -RUN npm install -g pnpm +# Install build dependencies +RUN apk add --no-cache python3 build-base chromium -# Copy application dependency manifests to the container image. -COPY --chown=node:node ./ ./ - -# Install application dependencies -RUN apk update -RUN apk add python3 build-base chromium - -# Set PYHTON env +# Set Python environment ENV PYTHON=/usr/bin/python3 -# Install packages dependencies for production. -RUN pnpm install +# Copy package files for dependency installation +COPY --chown=node:node package.json pnpm-lock.yaml pnpm-workspace.yaml lerna.json ./ +COPY --chown=node:node packages/server/package.json ./packages/server/ +COPY --chown=node:node shared/bigcapital-utils/package.json ./shared/bigcapital-utils/ +COPY --chown=node:node shared/pdf-templates/package.json ./shared/pdf-templates/ +COPY --chown=node:node shared/email-components/package.json ./shared/email-components/ +# Install all dependencies (including devDependencies for build) +RUN pnpm install --frozen-lockfile + +# Copy source code COPY --chown=node:node ./packages/server ./packages/server +COPY --chown=node:node ./shared/bigcapital-utils ./shared/bigcapital-utils +COPY --chown=node:node ./shared/pdf-templates ./shared/pdf-templates +COPY --chown=node:node ./shared/email-components ./shared/email-components -# # Creates a "dist" folder with the production build +# Build NestJS application RUN pnpm run build:server --skip-nx-cache -CMD [ "node", "./packages/server/build/index.js" ] \ No newline at end of file +# Stage 2: Production +FROM node:18.16.0-alpine AS production + +WORKDIR /app + +# Install pnpm for production +RUN npm install -g pnpm@8.10.2 + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 + +# Install build dependencies for native modules (bcrypt, etc.) +RUN apk add --no-cache python3 build-base + +# Set Python environment +ENV PYTHON=/usr/bin/python3 + +# Copy package files for production dependency installation +COPY --chown=nodejs:nodejs package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY --chown=nodejs:nodejs packages/server/package.json ./packages/server/ +COPY --chown=nodejs:nodejs shared/bigcapital-utils/package.json ./shared/bigcapital-utils/ +COPY --chown=nodejs:nodejs shared/pdf-templates/package.json ./shared/pdf-templates/ +COPY --chown=nodejs:nodejs shared/email-components/package.json ./shared/email-components/ + +# Copy .husky directory (needed for husky install command) +COPY --chown=nodejs:nodejs .husky ./.husky + +# Install only production dependencies +# Install husky temporarily so prepare script can run, then remove it +RUN pnpm add -D -w husky && \ + pnpm install --prod --frozen-lockfile && \ + pnpm remove -w husky && \ + # Remove build dependencies to reduce image size + apk del python3 build-base + +# Copy built application from builder stage +COPY --from=builder --chown=nodejs:nodejs /app/packages/server/dist ./packages/server/dist + +# Copy static assets (i18n, public, static directories) +COPY --from=builder --chown=nodejs:nodejs /app/packages/server/src/i18n ./packages/server/dist/i18n +COPY --from=builder --chown=nodejs:nodejs /app/packages/server/public ./packages/server/public +COPY --from=builder --chown=nodejs:nodejs /app/packages/server/static ./packages/server/static + +# Copy built shared packages (dist folders and package.json for module resolution) +COPY --from=builder --chown=nodejs:nodejs /app/shared/bigcapital-utils/dist ./shared/bigcapital-utils/dist +COPY --from=builder --chown=nodejs:nodejs /app/shared/pdf-templates/dist ./shared/pdf-templates/dist +COPY --from=builder --chown=nodejs:nodejs /app/shared/email-components/dist ./shared/email-components/dist + +# Set runtime environment variables (these should be provided at runtime via docker-compose or k8s) +ENV NODE_ENV=production +ENV NEW_RELIC_NO_CONFIG_FILE=true +ENV PORT=3000 + +# Switch to non-root user +USER nodejs + +# Expose port +EXPOSE 3000 + +# Health check - uses /api/system_db ping endpoint +HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/api/system_db', (r) => {process.exit(r.statusCode >= 200 && r.statusCode < 300 ? 0 : 1)}).on('error', () => process.exit(1))" + +# Start the application +CMD [ "node", "packages/server/dist/main.js" ] diff --git a/packages/server/src/common/config/index.ts b/packages/server/src/common/config/index.ts index fec0d23c6..4e6cae00c 100644 --- a/packages/server/src/common/config/index.ts +++ b/packages/server/src/common/config/index.ts @@ -17,6 +17,8 @@ import loops from './loops'; import bankfeed from './bankfeed'; import throttle from './throttle'; import cloud from './cloud'; +import redis from './redis'; +import queue from './queue'; export const config = [ app, @@ -38,4 +40,6 @@ export const config = [ loops, bankfeed, throttle, + redis, + queue, ]; diff --git a/packages/server/src/common/config/queue.ts b/packages/server/src/common/config/queue.ts new file mode 100644 index 000000000..2950101a0 --- /dev/null +++ b/packages/server/src/common/config/queue.ts @@ -0,0 +1,6 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('queue', () => ({ + host: process.env.QUEUE_HOST || 'localhost', + port: parseInt(process.env.QUEUE_PORT, 10) || 6379, +})); diff --git a/packages/server/src/modules/App/App.module.ts b/packages/server/src/modules/App/App.module.ts index e50bfcd91..859bdd9bd 100644 --- a/packages/server/src/modules/App/App.module.ts +++ b/packages/server/src/modules/App/App.module.ts @@ -137,8 +137,8 @@ import { AppThrottleModule } from './AppThrottle.module'; imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ connection: { - host: configService.get('QUEUE_HOST'), - port: configService.get('QUEUE_PORT'), + host: configService.get('queue.host'), + port: configService.get('queue.port'), }, }), inject: [ConfigService], @@ -158,8 +158,8 @@ import { AppThrottleModule } from './AppThrottle.module'; imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ config: { - host: configService.get('redis.host') || 'localhost', - port: configService.get('redis.port') || 6379, + host: configService.get('redis.host'), + port: configService.get('redis.port'), }, }), inject: [ConfigService], diff --git a/packages/webapp/Dockerfile b/packages/webapp/Dockerfile index a86d52488..88d2f5f6c 100644 --- a/packages/webapp/Dockerfile +++ b/packages/webapp/Dockerfile @@ -1,27 +1,47 @@ -FROM node:18.16.0-alpine as build - -USER root +# Stage 1: Build +FROM node:18.16.0-alpine AS builder WORKDIR /app -# Copy application dependency manifests to the container image. -COPY . . +# Install pnpm +RUN npm install -g pnpm@8.10.2 -# Install application dependencies -RUN apk update -RUN apk add python3 build-base chromium +# Install build dependencies +RUN apk add --no-cache python3 build-base chromium -# Set PYHTON env +# Set Python environment ENV PYTHON=/usr/bin/python3 -# Install pnpm packages dependencies -RUN npm install -g pnpm +# Copy package files for dependency installation +COPY --chown=node:node package.json pnpm-lock.yaml pnpm-workspace.yaml lerna.json ./ +COPY --chown=node:node packages/webapp/package.json ./packages/webapp/ +COPY --chown=node:node shared/bigcapital-utils/package.json ./shared/bigcapital-utils/ +COPY --chown=node:node shared/pdf-templates/package.json ./shared/pdf-templates/ +COPY --chown=node:node shared/email-components/package.json ./shared/email-components/ + +# Install all dependencies (including devDependencies for build) RUN pnpm install +# Copy source code for webapp and dependencies +COPY --chown=node:node ./packages/webapp ./packages/webapp +COPY --chown=node:node ./shared/bigcapital-utils ./shared/bigcapital-utils +COPY --chown=node:node ./shared/pdf-templates ./shared/pdf-templates +COPY --chown=node:node ./shared/email-components ./shared/email-components + # Build webapp package RUN pnpm run build:webapp -FROM nginx +# Stage 2: Nginx +FROM nginx:alpine -COPY ./packages/webapp/nginx/sites/default.conf /etc/nginx/conf.d/default.conf -COPY --from=build /app/packages/webapp/dist /usr/share/nginx/html +# Copy nginx configuration +COPY --chown=root:root ./packages/webapp/nginx/sites/default.conf /etc/nginx/conf.d/default.conf + +# Copy built webapp assets from builder stage +COPY --from=builder --chown=nginx:nginx /app/packages/webapp/dist /usr/share/nginx/html + +# Expose port +EXPOSE 80 + +# Nginx runs as nginx user by default, which is good for security +# No CMD needed as nginx base image already has it diff --git a/packages/webapp/src/containers/Alerts/Accounts/AccountBulkActivateAlert.tsx b/packages/webapp/src/containers/Alerts/Accounts/AccountBulkActivateAlert.tsx index 4d86963a7..2a10a0a36 100644 --- a/packages/webapp/src/containers/Alerts/Accounts/AccountBulkActivateAlert.tsx +++ b/packages/webapp/src/containers/Alerts/Accounts/AccountBulkActivateAlert.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import intl from 'react-intl-universal'; import { Intent, Alert } from '@blueprintjs/core'; -import { queryCache } from 'react-query'; +import { useQueryClient } from 'react-query'; import { FormattedMessage as T, AppToaster } from '@/components'; import { withAlertStoreConnect } from '@/containers/Alert/withAlertStoreConnect'; @@ -22,6 +22,7 @@ function AccountBulkActivateAlert({ requestBulkActivateAccounts, }) { const [isLoading, setLoading] = useState(false); + const queryClient = useQueryClient(); const selectedRowsCount = 0; // Handle alert cancel. @@ -38,9 +39,9 @@ function AccountBulkActivateAlert({ message: intl.get('the_accounts_has_been_successfully_activated'), intent: Intent.SUCCESS, }); - queryCache.invalidateQueries('accounts-table'); + queryClient.invalidateQueries('accounts-table'); }) - .catch((errors) => { }) + .catch((errors) => {}) .finally(() => { setLoading(false); closeAlert(name); diff --git a/packages/webapp/src/containers/Alerts/Accounts/AccountBulkInactivateAlert.tsx b/packages/webapp/src/containers/Alerts/Accounts/AccountBulkInactivateAlert.tsx index cb40d0a45..f5103c3bb 100644 --- a/packages/webapp/src/containers/Alerts/Accounts/AccountBulkInactivateAlert.tsx +++ b/packages/webapp/src/containers/Alerts/Accounts/AccountBulkInactivateAlert.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { FormattedMessage as T } from '@/components'; import intl from 'react-intl-universal'; import { Intent, Alert } from '@blueprintjs/core'; -import { queryCache } from 'react-query'; +import { useQueryClient } from 'react-query'; import { AppToaster } from '@/components'; // import { withAccountsActions } from '@/containers/Accounts/withAccountsTableActions'; @@ -22,8 +22,8 @@ function AccountBulkInactivateAlert({ closeAlert, }) { - const [isLoading, setLoading] = useState(false); + const queryClient = useQueryClient(); const selectedRowsCount = 0; // Handle alert cancel. @@ -39,9 +39,9 @@ function AccountBulkInactivateAlert({ message: intl.get('the_accounts_have_been_successfully_inactivated'), intent: Intent.SUCCESS, }); - queryCache.invalidateQueries('accounts-table'); + queryClient.invalidateQueries('accounts-table'); }) - .catch((errors) => { }) + .catch((errors) => {}) .finally(() => { setLoading(false); closeAlert(name); diff --git a/packages/webapp/src/containers/Alerts/Estimates/EstimateApproveAlert.tsx b/packages/webapp/src/containers/Alerts/Estimates/EstimateApproveAlert.tsx index 24a766ce3..2eb112807 100644 --- a/packages/webapp/src/containers/Alerts/Estimates/EstimateApproveAlert.tsx +++ b/packages/webapp/src/containers/Alerts/Estimates/EstimateApproveAlert.tsx @@ -3,7 +3,7 @@ import React, { useCallback } from 'react'; import intl from 'react-intl-universal'; import { AppToaster, FormattedMessage as T } from '@/components'; import { Intent, Alert } from '@blueprintjs/core'; -import { queryCache } from 'react-query'; +import { useQueryClient } from 'react-query'; import { useApproveEstimate } from '@/hooks/query'; @@ -25,6 +25,7 @@ function EstimateApproveAlert({ // #withAlertActions closeAlert, }) { + const queryClient = useQueryClient(); const { mutateAsync: deliverEstimateMutate, isLoading } = useApproveEstimate(); @@ -40,7 +41,7 @@ function EstimateApproveAlert({ message: intl.get('the_estimate_has_been_approved_successfully'), intent: Intent.SUCCESS, }); - queryCache.invalidateQueries('estimates-table'); + queryClient.invalidateQueries('estimates-table'); }) .catch((error) => {}) .finally(() => { diff --git a/packages/webapp/src/containers/Dialogs/BillNumberDialog/BillNumberDialogContent.tsx b/packages/webapp/src/containers/Dialogs/BillNumberDialog/BillNumberDialogContent.tsx index f93ce0116..44b1594b3 100644 --- a/packages/webapp/src/containers/Dialogs/BillNumberDialog/BillNumberDialogContent.tsx +++ b/packages/webapp/src/containers/Dialogs/BillNumberDialog/BillNumberDialogContent.tsx @@ -1,7 +1,7 @@ // @ts-nocheck import React from 'react'; import { DialogContent } from '@/components'; -import { useQuery, queryCache } from 'react-query'; +import { useQuery, useQueryClient } from 'react-query'; import ReferenceNumberForm from '@/containers/JournalNumber/ReferenceNumberForm'; @@ -31,6 +31,7 @@ function BillNumberDialogContent({ // #withBillsActions setBillNumberChanged, }) { + const queryClient = useQueryClient(); const fetchSettings = useQuery(['settings'], () => requestFetchOptions({})); const handleSubmitForm = (values, { setSubmitting }) => { @@ -45,7 +46,7 @@ function BillNumberDialogContent({ setBillNumberChanged(true); setTimeout(() => { - queryCache.invalidateQueries('settings'); + queryClient.invalidateQueries('settings'); }, 250); }) .catch(() => {