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
+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