Add Node.js/Express API service for booking backend
- 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:
@@ -29,3 +29,8 @@ config/local.json
|
|||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
|
|
||||||
|
# API runtime data
|
||||||
|
api/data/*.db
|
||||||
|
api/signed-contracts/*.pdf
|
||||||
|
api/.env
|
||||||
|
|||||||
@@ -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=
|
||||||
@@ -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"]
|
||||||
@@ -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;
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}`));
|
||||||
+20
-6
@@ -25,12 +25,26 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
|
|
||||||
# Optional: Watchtower for automatic updates
|
api:
|
||||||
# watchtower:
|
build:
|
||||||
# image: containrrr/watchtower
|
context: ./api
|
||||||
# volumes:
|
dockerfile: Dockerfile
|
||||||
# - /var/run/docker.sock:/var/run/docker.sock
|
container_name: lisilou-api
|
||||||
# command: --interval 300 lisilou-portfolio
|
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:
|
networks:
|
||||||
web:
|
web:
|
||||||
|
|||||||
+12
@@ -86,6 +86,18 @@ http {
|
|||||||
proxy_ssl_server_name on;
|
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
|
# Proxy endpoint for Immich share pages and API
|
||||||
location ~ ^/api/fetch-immich/(.+)$ {
|
location ~ ^/api/fetch-immich/(.+)$ {
|
||||||
resolver 8.8.8.8 1.1.1.1 valid=300s;
|
resolver 8.8.8.8 1.1.1.1 valid=300s;
|
||||||
|
|||||||
Reference in New Issue
Block a user