Fix healthchecks: install curl in backend and frontend images

This commit is contained in:
Agent Zero
2026-05-24 21:54:58 +00:00
commit cdf350c618
52 changed files with 7885 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
FROM node:22-alpine
RUN apk add --no-cache curl
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 5000
CMD ["node", "server.js"]
+22
View File
@@ -0,0 +1,22 @@
const { Sequelize } = require('sequelize');
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: process.env.DB_PATH || './database.sqlite',
logging: false,
});
const connectDB = async () => {
try {
await sequelize.authenticate();
console.log('Database connected.');
await sequelize.sync();
console.log('Database synced.');
} catch (err) {
console.error('Database connection error:', err);
// Retry after 5s
setTimeout(connectDB, 5000);
}
};
module.exports = { sequelize, connectDB };
+23
View File
@@ -0,0 +1,23 @@
const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const User = require('../models/User');
const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET || 'dev_secret_change_in_production',
};
passport.use(new JwtStrategy(opts, async (jwt_payload, done) => {
try {
const user = await User.findByPk(jwt_payload.id);
if (user) {
return done(null, user);
}
return done(null, false);
} catch (err) {
return done(err, false);
}
}));
module.exports = passport;
+6
View File
@@ -0,0 +1,6 @@
const passport = require('passport');
// Protect routes with JWT
const authenticate = passport.authenticate('jwt', { session: false });
module.exports = authenticate;
+33
View File
@@ -0,0 +1,33 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const User = require('./User');
const Block = sequelize.define('Block', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
svg_data: {
type: DataTypes.TEXT,
allowNull: false,
},
category: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: 'Allgemein',
},
is_public: {
type: DataTypes.BOOLEAN,
defaultValue: true,
},
});
Block.belongsTo(User, { foreignKey: 'userId', allowNull: true, onDelete: 'SET NULL' });
User.hasMany(Block, { foreignKey: 'userId' });
module.exports = Block;
+29
View File
@@ -0,0 +1,29 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const User = require('./User');
const Drawing = sequelize.define('Drawing', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'Unbenannt',
},
canvas_json: {
type: DataTypes.TEXT,
allowNull: true,
},
thumbnail: {
type: DataTypes.TEXT,
allowNull: true,
},
});
Drawing.belongsTo(User, { foreignKey: 'userId', onDelete: 'CASCADE' });
User.hasMany(Drawing, { foreignKey: 'userId' });
module.exports = Drawing;
+31
View File
@@ -0,0 +1,31 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const bcrypt = require('bcryptjs');
const User = sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
email: {
type: DataTypes.STRING,
unique: true,
allowNull: false,
validate: { isEmail: true },
},
password: {
type: DataTypes.STRING,
allowNull: false,
},
});
User.beforeCreate(async (user) => {
user.password = await bcrypt.hash(user.password, 10);
});
User.prototype.validatePassword = async function (password) {
return bcrypt.compare(password, this.password);
};
module.exports = User;
+1903
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bcryptjs": "^3.0.3",
"cors": "^2.8.6",
"dxf-parser": "^1.1.2",
"express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
"multer": "^2.1.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"sequelize": "^6.37.8",
"sqlite3": "^6.0.1"
}
}
@@ -0,0 +1,6 @@
{
"name": "Dimension Tool",
"version": "1.0.0",
"description": "Fügt ein Bemaßungswerkzeug für horizontale und vertikale Messungen hinzu.",
"author": "Agent Zero"
}
+53
View File
@@ -0,0 +1,53 @@
const express = require('express');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const authenticate = require('../middleware/auth');
const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET || 'dev_secret_change_in_production';
// Register
router.post('/register', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
const existing = await User.findOne({ where: { email } });
if (existing) {
return res.status(409).json({ error: 'User already exists' });
}
const user = await User.create({ email, password });
const token = jwt.sign({ id: user.id, email: user.email }, JWT_SECRET, { expiresIn: '7d' });
res.status(201).json({ token, user: { id: user.id, email: user.email } });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Registration failed' });
}
});
// Login
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
const user = await User.findOne({ where: { email } });
if (!user || !(await user.validatePassword(password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign({ id: user.id, email: user.email }, JWT_SECRET, { expiresIn: '7d' });
res.json({ token, user: { id: user.id, email: user.email } });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Login failed' });
}
});
// Get current user
router.get('/me', authenticate, async (req, res) => {
res.json({ id: req.user.id, email: req.user.email });
});
module.exports = router;
+59
View File
@@ -0,0 +1,59 @@
const express = require('express');
const authenticate = require('../middleware/auth');
const Block = require('../models/Block');
const router = express.Router();
// Public blocks (no auth for listing)
router.get('/', async (req, res) => {
try {
const blocks = await Block.findAll({
where: { is_public: true },
attributes: ['id', 'name', 'svg_data', 'category'],
});
res.json(blocks);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to list blocks' });
}
});
// Protected: create a new block (requires auth)
router.post('/', authenticate, async (req, res) => {
try {
const { name, svg_data, category, is_public } = req.body;
if (!name || !svg_data) {
return res.status(400).json({ error: 'Name and svg_data required' });
}
const block = await Block.create({
name,
svg_data,
category: category || 'Allgemein',
is_public: is_public !== undefined ? is_public : true,
userId: req.user.id,
});
res.status(201).json(block);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to create block' });
}
});
// Protected: delete block (only owner)
router.delete('/:id', authenticate, async (req, res) => {
try {
const block = await Block.findOne({
where: { id: req.params.id, userId: req.user.id },
});
if (!block) {
return res.status(404).json({ error: 'Block not found or not authorized' });
}
await block.destroy();
res.json({ message: 'Block deleted' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to delete block' });
}
});
module.exports = router;
+94
View File
@@ -0,0 +1,94 @@
const express = require('express');
const authenticate = require('../middleware/auth');
const Drawing = require('../models/Drawing');
const router = express.Router();
// All routes require authentication
router.use(authenticate);
// List drawings for current user
router.get('/', async (req, res) => {
try {
const drawings = await Drawing.findAll({
where: { userId: req.user.id },
attributes: ['id', 'name', 'updatedAt'],
order: [['updatedAt', 'DESC']],
});
res.json(drawings);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to list drawings' });
}
});
// Create new drawing
router.post('/', async (req, res) => {
try {
const { name, canvas_json } = req.body;
const drawing = await Drawing.create({
name: name || 'Unbenannt',
canvas_json: canvas_json || '',
userId: req.user.id,
});
res.status(201).json(drawing);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to create drawing' });
}
});
// Get single drawing
router.get('/:id', async (req, res) => {
try {
const drawing = await Drawing.findOne({
where: { id: req.params.id, userId: req.user.id },
});
if (!drawing) {
return res.status(404).json({ error: 'Drawing not found' });
}
res.json(drawing);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to get drawing' });
}
});
// Update drawing
router.put('/:id', async (req, res) => {
try {
const drawing = await Drawing.findOne({
where: { id: req.params.id, userId: req.user.id },
});
if (!drawing) {
return res.status(404).json({ error: 'Drawing not found' });
}
const { name, canvas_json } = req.body;
if (name !== undefined) drawing.name = name;
if (canvas_json !== undefined) drawing.canvas_json = canvas_json;
await drawing.save();
res.json(drawing);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to update drawing' });
}
});
// Delete drawing
router.delete('/:id', async (req, res) => {
try {
const drawing = await Drawing.findOne({
where: { id: req.params.id, userId: req.user.id },
});
if (!drawing) {
return res.status(404).json({ error: 'Drawing not found' });
}
await drawing.destroy();
res.json({ message: 'Drawing deleted' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to delete drawing' });
}
});
module.exports = router;
+101
View File
@@ -0,0 +1,101 @@
const express = require('express');
const authenticate = require('../middleware/auth');
const multer = require('multer');
const { parseString } = require('dxf-parser');
const router = express.Router();
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 10 * 1024 * 1024 } });
// Upload DXF file -> parse and return JSON representation (Fabric.js compatible)
router.post('/import', authenticate, upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const dxfContent = req.file.buffer.toString('utf-8');
console.log('DXF file size:', dxfContent.length);
const parsed = parseString(dxfContent);
// Convert parsed entities to Fabric.js objects (simplified)
const fabricObjects = [];
if (parsed && parsed.entities) {
parsed.entities.forEach(entity => {
const type = entity.type;
if (type === 'LINE') {
fabricObjects.push({
type: 'line',
x1: entity.start.x, y1: entity.start.y,
x2: entity.end.x, y2: entity.end.y,
stroke: '#000', strokeWidth: 1,
});
} else if (type === 'CIRCLE') {
fabricObjects.push({
type: 'circle',
left: entity.center.x - entity.radius,
top: entity.center.y - entity.radius,
radius: entity.radius,
fill: 'transparent', stroke: '#000',
});
} else if (type === 'ARC') {
// simplified arc handling
fabricObjects.push({
type: 'path',
path: `M ${entity.center.x} ${entity.center.y}`, // placeholder
stroke: '#000', strokeWidth: 1,
});
} else if (type === 'POLYLINE' || type === 'LWPOLYLINE') {
// convert vertices to polygon
if (entity.vertices && entity.vertices.length >= 2) {
const points = entity.vertices.map(v => ({ x: v.x, y: v.y }));
fabricObjects.push({
type: 'polygon',
points,
stroke: '#000', strokeWidth: 1, fill: 'transparent',
});
}
}
});
}
res.json({ objects: fabricObjects });
} catch (err) {
console.error('DXF import error:', err);
res.status(500).json({ error: 'Failed to parse DXF file: ' + err.message });
}
});
// Export canvas JSON to DXF string
router.post('/export', authenticate, (req, res) => {
try {
const { objects } = req.body;
if (!objects || !Array.isArray(objects)) {
return res.status(400).json({ error: 'Invalid objects array' });
}
// Build DXF content
let dxf = '0\nSECTION\n2\nHEADER\n0\nENDSEC\n';
dxf += '0\nSECTION\n2\nTABLES\n0\nENDSEC\n';
dxf += '0\nSECTION\n2\nBLOCKS\n0\nENDSEC\n';
dxf += '0\nSECTION\n2\nENTITIES\n';
objects.forEach(obj => {
if (obj.type === 'line') {
dxf += `0\nLINE\n8\n0\n10\n${obj.x1}\n20\n${obj.y1}\n11\n${obj.x2}\n21\n${obj.y2}\n`;
} else if (obj.type === 'circle') {
const cx = obj.left + obj.radius;
const cy = obj.top + obj.radius;
dxf += `0\nCIRCLE\n8\n0\n10\n${cx}\n20\n${cy}\n40\n${obj.radius}\n`;
} else if (obj.type === 'polygon') {
// LWPOLYLINE
dxf += `0\nLWPOLYLINE\n8\n0\n90\n${obj.points.length}\n`;
obj.points.forEach((pt, i) => {
dxf += `10\n${pt.x}\n20\n${pt.y}\n`;
});
}
});
dxf += '0\nENDSEC\n0\nEOF\n';
res.setHeader('Content-Type', 'application/dxf');
res.setHeader('Content-Disposition', 'attachment; filename=drawing.dxf');
res.send(dxf);
} catch (err) {
console.error('DXF export error:', err);
res.status(500).json({ error: 'Export failed' });
}
});
module.exports = router;
+56
View File
@@ -0,0 +1,56 @@
const express = require('express');
const fs = require('fs');
const path = require('path');
const router = express.Router();
const PLUGINS_DIR = path.join(__dirname, '..', 'plugins');
// Ensure plugins directory exists
if (!fs.existsSync(PLUGINS_DIR)) {
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
}
// List all available plugins
router.get('/', (req, res) => {
try {
if (!fs.existsSync(PLUGINS_DIR)) {
return res.json([]);
}
const plugins = [];
const dirs = fs.readdirSync(PLUGINS_DIR);
for (const dir of dirs) {
const pluginPath = path.join(PLUGINS_DIR, dir);
const manifestPath = path.join(pluginPath, 'plugin.json');
if (fs.existsSync(manifestPath)) {
try {
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
plugins.push({ id: dir, ...manifest });
} catch (e) {
console.warn(`Invalid plugin manifest: ${dir}`);
}
}
}
res.json(plugins);
} catch (err) {
console.error('Plugin list error:', err);
res.status(500).json({ error: 'Failed to list plugins' });
}
});
// Serve plugin code as JavaScript module
router.get('/:id/code', (req, res) => {
try {
const pluginDir = path.join(PLUGINS_DIR, req.params.id);
const codePath = path.join(pluginDir, 'plugin.js');
if (!fs.existsSync(codePath)) {
return res.status(404).json({ error: 'Plugin code not found' });
}
res.setHeader('Content-Type', 'application/javascript');
res.sendFile(codePath);
} catch (err) {
console.error('Plugin code error:', err);
res.status(500).json({ error: 'Failed to load plugin code' });
}
});
module.exports = router;
+132
View File
@@ -0,0 +1,132 @@
const { sequelize, connectDB } = require('./config/database');
const Block = require('./models/Block');
const eventBlocks = [
{
name: 'Stuhl (Standard)',
category: 'Bestuhlung',
is_public: true,
svg_data: `<svg viewBox="0 0 40 40">
<rect x="5" y="5" width="30" height="30" rx="2" fill="#4a90d9" stroke="#2c5f8a" stroke-width="2"/>
<rect x="10" y="28" width="20" height="4" rx="1" fill="#d4e6f1"/>
<circle cx="20" cy="18" r="8" fill="#d4e6f1" stroke="#2c5f8a" stroke-width="1.5"/>
</svg>`
},
{
name: 'Tisch (Rechteck 180x80)',
category: 'Möbel',
is_public: true,
svg_data: `<svg viewBox="0 0 180 80">
<rect x="0" y="0" width="180" height="80" rx="4" fill="#e8d5b7" stroke="#8b7355" stroke-width="2"/>
<rect x="5" y="5" width="170" height="70" rx="2" fill="#f5ead0" stroke="#c4a882" stroke-width="1"/>
</svg>`
},
{
name: 'Tisch (Rund Ø120)',
category: 'Möbel',
is_public: true,
svg_data: `<svg viewBox="0 0 120 120">
<circle cx="60" cy="60" r="58" fill="#e8d5b7" stroke="#8b7355" stroke-width="2"/>
<circle cx="60" cy="60" r="50" fill="#f5ead0" stroke="#c4a882" stroke-width="1"/>
</svg>`
},
{
name: 'Bühne (Modular 200x100)',
category: 'Bühne',
is_public: true,
svg_data: `<svg viewBox="0 0 200 100">
<rect x="0" y="0" width="200" height="100" fill="#8b7355" stroke="#6b5335" stroke-width="2"/>
<line x1="0" y1="25" x2="200" y2="25" stroke="#a08060" stroke-width="1"/>
<line x1="0" y1="50" x2="200" y2="50" stroke="#a08060" stroke-width="1"/>
<line x1="0" y1="75" x2="200" y2="75" stroke="#a08060" stroke-width="1"/>
<line x1="50" y1="0" x2="50" y2="100" stroke="#a08060" stroke-width="1"/>
<line x1="100" y1="0" x2="100" y2="100" stroke="#a08060" stroke-width="1"/>
<line x1="150" y1="0" x2="150" y2="100" stroke="#a08060" stroke-width="1"/>
</svg>`
},
{
name: 'Stehtisch',
category: 'Möbel',
is_public: true,
svg_data: `<svg viewBox="0 0 60 60">
<circle cx="30" cy="30" r="28" fill="#e8d5b7" stroke="#8b7355" stroke-width="2"/>
<circle cx="30" cy="30" r="24" fill="#f5ead0" stroke="#c4a882" stroke-width="1"/>
<circle cx="30" cy="30" r="5" fill="#8b7355"/>
</svg>`
},
{
name: 'Barhocker',
category: 'Bestuhlung',
is_public: true,
svg_data: `<svg viewBox="0 0 30 30">
<circle cx="15" cy="12" r="10" fill="#4a90d9" stroke="#2c5f8a" stroke-width="2"/>
<rect x="12" y="20" width="6" height="8" fill="#7f8c8d"/>
</svg>`
},
{
name: 'Mischpult',
category: 'Technik',
is_public: true,
svg_data: `<svg viewBox="0 0 120 60">
<rect x="0" y="0" width="120" height="60" rx="3" fill="#2c3e50" stroke="#1a252f" stroke-width="2"/>
<rect x="5" y="5" width="110" height="15" fill="#34495e" stroke="#2c3e50" stroke-width="1"/>
<circle cx="20" cy="40" r="5" fill="#e74c3c"/>
<circle cx="40" cy="40" r="5" fill="#e67e22"/>
<circle cx="60" cy="40" r="5" fill="#f1c40f"/>
<circle cx="80" cy="40" r="5" fill="#3498db"/>
<circle cx="100" cy="40" r="5" fill="#2ecc71"/>
</svg>`
},
{
name: 'Lautsprecher',
category: 'Technik',
is_public: true,
svg_data: `<svg viewBox="0 0 40 60">
<rect x="0" y="0" width="40" height="60" rx="3" fill="#34495e" stroke="#1a252f" stroke-width="2"/>
<rect x="8" y="8" width="24" height="25" rx="12" fill="#1a252f"/>
<circle cx="20" cy="20" r="6" fill="#7f8c8d"/>
<rect x="8" y="40" width="24" height="5" rx="2" fill="#1a252f"/>
</svg>`
},
{
name: 'Trennwand',
category: 'Raum',
is_public: true,
svg_data: `<svg viewBox="0 0 10 100">
<rect x="0" y="0" width="10" height="100" fill="#bdc3c7" stroke="#7f8c8d" stroke-width="1"/>
<line x1="2" y1="0" x2="2" y2="100" stroke="#95a5a6" stroke-width="0.5"/>
<line x1="5" y1="0" x2="5" y2="100" stroke="#95a5a6" stroke-width="0.5"/>
<line x1="8" y1="0" x2="8" y2="100" stroke="#95a5a6" stroke-width="0.5"/>
</svg>`
},
{
name: 'Notausgang-Schild',
category: 'Sicherheit',
is_public: true,
svg_data: `<svg viewBox="0 0 40 20">
<rect x="0" y="0" width="40" height="20" rx="2" fill="#27ae60" stroke="#1e8449" stroke-width="1"/>
<polygon points="8,5 3,10 8,15 7,15 2,10 7,5" fill="white"/>
<text x="20" y="15" font-size="8" fill="white" text-anchor="middle" font-family="Arial">EXIT</text>
</svg>`
},
];
async function seed() {
try {
await connectDB();
// Clear existing blocks
await Block.destroy({ where: {} });
console.log('Existing blocks cleared.');
// Insert new blocks
for (const block of eventBlocks) {
await Block.create(block);
}
console.log(`${eventBlocks.length} blocks created successfully.`);
process.exit(0);
} catch (err) {
console.error('Seed failed:', err);
process.exit(1);
}
}
seed();
+30
View File
@@ -0,0 +1,30 @@
const express = require('express');
const cors = require('cors');
const passport = require('passport');
require('./config/passport'); // Load JWT strategy
const { sequelize, connectDB } = require('./config/database');
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
app.use(cors());
app.use(express.json({ limit: '50mb' }));
app.use(passport.initialize());
// Routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/drawings', require('./routes/drawings'));
app.use('/api/blocks', require('./routes/blocks'));
app.use('/api/dxf', require('./routes/dxf'));
app.use('/api/plugins', require('./routes/plugins'));
// Health check
app.get('/api/health', (req, res) => res.json({ status: 'ok' }));
// Connect DB and start
connectDB().then(() => {
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
}).catch(err => {
console.error('Unable to start server:', err);
});