Add Node.js/Express API service for booking backend
Build and Deploy / build (push) Failing after 20s
Build and Deploy / deploy (push) Has been skipped

- New api/ service: Express + better-sqlite3, health check at GET /api/health,
  POST /api/bookings stores sessions with all required fields
- Dockerfile with Alpine build deps for native sqlite3 module
- docker-compose.yml: api service with volume mounts for data, signed-contracts, contracts
- nginx.conf: proxy /api/ to api:3001; Immich regex locations remain higher priority
- .gitignore: exclude SQLite db file and signed PDFs from version control

Closes #1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 22:30:18 -06:00
parent 4122e2d128
commit e824be7e5d
11 changed files with 155 additions and 6 deletions
+5
View File
@@ -29,3 +29,8 @@ config/local.json
# Docker
docker-compose.override.yml
# API runtime data
api/data/*.db
api/signed-contracts/*.pdf
api/.env
+21
View File
@@ -0,0 +1,21 @@
PORT=3001
CORS_ORIGIN=http://localhost:8080
# n8n webhook for email notifications
N8N_WEBHOOK_URL=
# Google Calendar (issue #3)
GOOGLE_CALENDAR_ID=
GOOGLE_SERVICE_ACCOUNT_JSON=
# Contract PDF signature placement (issue #6)
CONTRACT_SIG_X=100
CONTRACT_SIG_Y=700
CONTRACT_SIG_PAGE=1
# OIDC / Authentik (issue #10)
SESSION_SECRET=
OIDC_ISSUER=
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_REDIRECT_URI=
+16
View File
@@ -0,0 +1,16 @@
FROM node:20-alpine
RUN apk add --no-cache python3 make g++
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN mkdir -p data signed-contracts contracts
EXPOSE 3001
CMD ["node", "server.js"]
View File
View File
+31
View File
@@ -0,0 +1,31 @@
const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
const dataDir = path.join(__dirname, 'data');
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
const db = new Database(path.join(dataDir, 'bookings.db'));
db.exec(`
CREATE TABLE IF NOT EXISTS bookings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT DEFAULT (datetime('now')),
client_name TEXT,
client_email TEXT,
client_phone TEXT,
client_sub TEXT,
session_date TEXT,
session_type TEXT,
session_length TEXT,
location TEXT,
contract_signed_at TEXT,
contract_pdf_path TEXT,
payment_status TEXT DEFAULT 'pending',
payment_notified_at TEXT,
status TEXT DEFAULT 'pending',
notes TEXT
)
`);
module.exports = db;
+15
View File
@@ -0,0 +1,15 @@
{
"name": "lisilou-api",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js"
},
"dependencies": {
"better-sqlite3": "^9.4.3",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.3"
}
}
+35
View File
@@ -0,0 +1,35 @@
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const db = require('./db');
const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors({ origin: process.env.CORS_ORIGIN || '*', credentials: true }));
app.use(express.json({ limit: '10mb' }));
app.get('/api/health', (req, res) => {
res.json({ ok: true });
});
app.post('/api/bookings', (req, res) => {
const {
client_name, client_email, client_phone, client_sub,
session_date, session_type, session_length, location,
} = req.body;
if (!client_email || !session_date || !session_type || !session_length) {
return res.status(400).json({ error: 'Missing required fields' });
}
const result = db.prepare(`
INSERT INTO bookings
(client_name, client_email, client_phone, client_sub, session_date, session_type, session_length, location)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(client_name, client_email, client_phone, client_sub, session_date, session_type, session_length, location);
res.status(201).json({ id: result.lastInsertRowid });
});
app.listen(PORT, () => console.log(`API listening on port ${PORT}`));
View File
+20 -6
View File
@@ -25,12 +25,26 @@ services:
networks:
- web
# Optional: Watchtower for automatic updates
# watchtower:
# image: containrrr/watchtower
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock
# command: --interval 300 lisilou-portfolio
api:
build:
context: ./api
dockerfile: Dockerfile
container_name: lisilou-api
restart: unless-stopped
env_file:
- ./api/.env
volumes:
- ./api/data:/app/data
- ./api/signed-contracts:/app/signed-contracts
- ./api/contracts:/app/contracts:ro
networks:
- web
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3001/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
networks:
web:
+12
View File
@@ -86,6 +86,18 @@ http {
proxy_ssl_server_name on;
}
# Booking API — proxy to Node.js service
# Immich regex locations above take priority for /api/immich-proxy and /api/fetch-immich/
location /api/ {
proxy_pass http://api:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 30s;
}
# Proxy endpoint for Immich share pages and API
location ~ ^/api/fetch-immich/(.+)$ {
resolver 8.8.8.8 1.1.1.1 valid=300s;