Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Keep the build context small and the image clean/secure.

# Deps — reinstalled inside the image from the lockfile.
node_modules
npm-debug.log*

# Local SQLite database & uploads — these must NOT be baked into the image.
# At runtime they live on the mounted named volume (/app/data).
data
uploads
*.db
*.sqlite
*.sqlite3

# Secrets — never copy real env files into the image.
.env
.env.local
.env.*.local

# VCS / CI / editor noise.
.git
.gitignore
.github
.idea
.vscode
*.swp
*.swo

# Tests are not needed at runtime (run them in CI instead).
test
jest.config.js
coverage

# Mobile shells & their build artifacts (server image doesn't need them).
android
ios
capacitor.config.json
*.apk
*.aab
*.ipa

# Logs & OS cruft.
logs
*.log
.DS_Store
Thumbs.db

# Docker / deploy meta (no need to copy into the image).
Dockerfile
.dockerignore
docker-compose.yml
scripts/deploy.sh
README.md
65 changes: 65 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# ============================================================================
# FINMAN — environment configuration
# Copy to `.env` and fill in real values: cp .env.example .env
# NEVER commit a real .env (it is gitignored). Generate strong secrets, e.g.:
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# ============================================================================

# ---- Core server ----------------------------------------------------------
# Port the app listens on (host port in docker-compose mirrors this).
PORT=3000
# development | production. In production the secrets below are REQUIRED
# (validateEnv() and config.js fail-fast instead of auto-generating dev keys).
NODE_ENV=production

# ---- Secrets (REQUIRED in production) -------------------------------------
# JWT signing secret for Passport-JWT auth tokens.
# gen: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
JWT_SECRET=change-me-32-byte-hex
# express-session cookie secret.
# gen: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
SESSION_SECRET=change-me-32-byte-hex
# AES-256 key for encrypting stored bank API tokens. MUST be 64 hex chars (32 bytes).
# gen: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
ENCRYPTION_KEY=change-me-64-hex-chars

# ---- Database -------------------------------------------------------------
# SQLite file path. Default for local dev is ./data/finance.db.
# In Docker this is forced to /app/data/finance.db (persistent named volume).
DATABASE_PATH=./data/finance.db

# ---- CORS (optional) ------------------------------------------------------
# Allowed origin for browser requests. Leave unset to use the per-env default
# (http://localhost:3000 in dev, disabled in production). Set to your domain
# in production, e.g. https://finman.example.com
# CORS_ORIGIN=https://finman.example.com

# ---- Logging (optional) ---------------------------------------------------
# pino log level: trace|debug|info|warn|error|fatal|silent. Default: info.
# LOG_LEVEL=info

# ---- AI assistant (optional — feature stays 503 until configured) ---------
# Provider-agnostic AI layer (lib/ai/provider.js).
# AI_PROVIDER: anthropic | openai | ollama (default: anthropic)
AI_PROVIDER=anthropic
# API key for the chosen provider. Without it, AI endpoints return
# 503 AI_NOT_CONFIGURED. (Ollama local typically needs no key.)
AI_API_KEY=
# Model id, e.g. claude-3-5-sonnet-latest | gpt-4o-mini | llama3.1
AI_MODEL=
# Override the provider base URL (e.g. http://localhost:11434 for Ollama,
# or a proxy/gateway). Optional; provider defaults are used when empty.
AI_BASE_URL=

# ---- Bank auto-sync (optional) --------------------------------------------
# Master switch for scheduled bank synchronization (Monobank/Revolut/Tinkoff).
# Set to "true" to enable; per-bank access tokens are added by each user in-app
# (stored encrypted with ENCRYPTION_KEY). Manual CSV import works without this.
SYNC_ENABLED=false

# ---- Billing / Stripe (optional — billing stays disabled until set) -------
# Stripe secret key (sk_live_... / sk_test_...). Required for the billing
# (subscription tier upgrade) flow; without it billing endpoints are inactive.
STRIPE_SECRET_KEY=
# Stripe webhook signing secret (whsec_...) for verifying webhook events.
STRIPE_WEBHOOK_SECRET=
60 changes: 60 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: CI

# Run on pushes and PRs targeting the main feature branches.
on:
push:
branches: [main, master, feat/finman-overhaul]
pull_request:

# Cancel superseded runs on the same ref to save CI minutes.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Lint deps & run tests (Node 22)
runs-on: ubuntu-latest

env:
NODE_ENV: test

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Node.js 22
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

# Reproducible install from the lockfile (includes devDeps: jest/supertest).
- name: Install dependencies
run: npm ci

# Jest + Supertest. package.json "test" => "jest --runInBand".
# The test harness binds each suite to an isolated temp SQLite DB.
- name: Run tests
run: npm test

docker-build:
name: Build Docker image
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

# Sanity-check that the production image builds. Not pushed anywhere.
- name: Build image
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: finman:ci
cache-from: type=gha
cache-to: type=gha,mode=max
64 changes: 64 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# syntax=docker/dockerfile:1
# ============================================================================
# FINMAN — multi-stage production image
# Stage 1 (deps): install PROD-only node_modules, rebuilding the sqlite3
# native addon from source if no prebuilt binary matches.
# Stage 2 (runtime): slim image, copies node_modules + app, runs as non-root,
# persists the SQLite DB under /app/data (mount a volume!).
# ============================================================================

# ---- Stage 1: dependencies ------------------------------------------------
FROM node:22-slim AS deps
WORKDIR /app

# Toolchain needed to (re)build native addons like sqlite3 if a prebuilt
# binary is unavailable for this platform/glibc. Kept ONLY in the deps stage
# so the final image stays lean.
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Install prod deps against the lockfile for reproducible builds.
# npm rebuild sqlite3 forces a source build if the downloaded prebuilt
# doesn't load (belt-and-suspenders; usually the prebuilt just works).
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --no-audit --no-fund \
&& npm rebuild sqlite3 --build-from-source --omit=dev || npm rebuild sqlite3 \
&& npm cache clean --force

# ---- Stage 2: runtime -----------------------------------------------------
FROM node:22-slim AS runtime
WORKDIR /app

ENV NODE_ENV=production \
PORT=3000 \
DATABASE_PATH=/app/data/finance.db

# wget is used by the HEALTHCHECK below; node:22-slim ships without it.
RUN apt-get update \
&& apt-get install -y --no-install-recommends wget \
&& rm -rf /var/lib/apt/lists/*

# Bring in the already-built dependencies from the deps stage.
COPY --from=deps /app/node_modules ./node_modules

# Copy the application source. .dockerignore keeps node_modules, the local
# data/ dir, mobile build output, tests and secrets out of the image.
COPY . .

# Persisted SQLite database lives here. Create it up front and hand ownership
# to the non-root user so the app can write even on a fresh named volume.
RUN mkdir -p /app/data \
&& chown -R node:node /app

# Drop privileges — never run the app as root.
USER node

EXPOSE 3000

# Liveness: hit the real /api/health endpoint (provided by routes/health.js).
# Fails (exit 1) if the server is not answering with HTTP 200.
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD wget --no-verbose --tries=1 --spider "http://127.0.0.1:${PORT}/api/health" || exit 1

CMD ["node", "server.js"]
Loading
Loading