first commit
This commit is contained in:
15
backend/Dockerfile
Normal file
15
backend/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install --production
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
19
backend/package.json
Normal file
19
backend/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "fb-tracker-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend for Facebook post tracker",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
}
|
||||
616
backend/server.js
Normal file
616
backend/server.js
Normal file
@@ -0,0 +1,616 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const Database = require('better-sqlite3');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
const MAX_PROFILES = 5;
|
||||
const DEFAULT_PROFILE_NAMES = {
|
||||
1: 'Profil 1',
|
||||
2: 'Profil 2',
|
||||
3: 'Profil 3',
|
||||
4: 'Profil 4',
|
||||
5: 'Profil 5'
|
||||
};
|
||||
|
||||
const screenshotDir = path.join(__dirname, 'data', 'screenshots');
|
||||
if (!fs.existsSync(screenshotDir)) {
|
||||
fs.mkdirSync(screenshotDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Middleware - Enhanced CORS for extension
|
||||
app.use(cors({
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||
credentials: true
|
||||
}));
|
||||
// Allow larger payloads because screenshots from high-res monitors can easily exceed 10 MB
|
||||
app.use(express.json({ limit: '30mb' }));
|
||||
|
||||
// Additional CORS headers for extension compatibility
|
||||
app.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
next();
|
||||
});
|
||||
|
||||
// Database setup
|
||||
const dbPath = path.join(__dirname, 'data', 'tracker.db');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
function clampTargetCount(value) {
|
||||
const parsed = parseInt(value, 10);
|
||||
if (Number.isNaN(parsed)) {
|
||||
return 1;
|
||||
}
|
||||
return Math.min(MAX_PROFILES, Math.max(1, parsed));
|
||||
}
|
||||
|
||||
function validateTargetCount(value) {
|
||||
const parsed = parseInt(value, 10);
|
||||
if (Number.isNaN(parsed) || parsed < 1 || parsed > MAX_PROFILES) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function sanitizeProfileNumber(value) {
|
||||
const parsed = parseInt(value, 10);
|
||||
if (Number.isNaN(parsed) || parsed < 1 || parsed > MAX_PROFILES) {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function getProfileName(profileNumber) {
|
||||
return DEFAULT_PROFILE_NAMES[profileNumber] || `Profil ${profileNumber}`;
|
||||
}
|
||||
|
||||
function getRequiredProfiles(targetCount) {
|
||||
const count = clampTargetCount(targetCount);
|
||||
return Array.from({ length: count }, (_, index) => index + 1);
|
||||
}
|
||||
|
||||
function buildProfileStatuses(requiredProfiles, checks) {
|
||||
const validChecks = checks
|
||||
.map(check => {
|
||||
const profileNumber = sanitizeProfileNumber(check.profile_number);
|
||||
if (!profileNumber) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...check,
|
||||
profile_number: profileNumber,
|
||||
profile_name: getProfileName(profileNumber)
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const completedSet = new Set(validChecks.map(check => check.profile_number));
|
||||
const checkByProfile = new Map(validChecks.map(check => [check.profile_number, check]));
|
||||
|
||||
const statuses = requiredProfiles.map((profileNumber, index) => {
|
||||
const prerequisites = requiredProfiles.slice(0, index);
|
||||
const prerequisitesMet = prerequisites.every(num => completedSet.has(num));
|
||||
const isChecked = completedSet.has(profileNumber);
|
||||
return {
|
||||
profile_number: profileNumber,
|
||||
profile_name: getProfileName(profileNumber),
|
||||
status: isChecked ? 'done' : (prerequisitesMet ? 'available' : 'locked'),
|
||||
checked_at: isChecked && checkByProfile.get(profileNumber)
|
||||
? checkByProfile.get(profileNumber).checked_at
|
||||
: null
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
statuses,
|
||||
completedChecks: validChecks,
|
||||
completedSet
|
||||
};
|
||||
}
|
||||
|
||||
function recalcCheckedCount(postId) {
|
||||
const post = db.prepare('SELECT id, target_count FROM posts WHERE id = ?').get(postId);
|
||||
if (!post) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checks = db.prepare('SELECT id, profile_number, checked_at FROM checks WHERE post_id = ?').all(postId);
|
||||
const requiredProfiles = getRequiredProfiles(post.target_count);
|
||||
const { statuses } = buildProfileStatuses(requiredProfiles, checks);
|
||||
const checkedCount = statuses.filter(status => status.status === 'done').length;
|
||||
|
||||
db.prepare('UPDATE posts SET checked_count = ? WHERE id = ?').run(checkedCount, postId);
|
||||
if (post.target_count !== requiredProfiles.length) {
|
||||
db.prepare('UPDATE posts SET target_count = ? WHERE id = ?').run(requiredProfiles.length, postId);
|
||||
}
|
||||
|
||||
return checkedCount;
|
||||
}
|
||||
|
||||
// Initialize database tables
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS posts (
|
||||
id TEXT PRIMARY KEY,
|
||||
url TEXT NOT NULL UNIQUE,
|
||||
title TEXT,
|
||||
target_count INTEGER NOT NULL,
|
||||
checked_count INTEGER DEFAULT 0,
|
||||
screenshot_path TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS checks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
post_id TEXT NOT NULL,
|
||||
profile_number INTEGER,
|
||||
checked_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (post_id) REFERENCES posts(id)
|
||||
);
|
||||
`);
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS profile_state (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
profile_number INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
const ensureColumn = (table, column, definition) => {
|
||||
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
|
||||
if (!columns.some(col => col.name === column)) {
|
||||
db.exec(`ALTER TABLE ${table} ADD COLUMN ${definition}`);
|
||||
}
|
||||
};
|
||||
|
||||
ensureColumn('posts', 'checked_count', 'checked_count INTEGER DEFAULT 0');
|
||||
ensureColumn('posts', 'screenshot_path', 'screenshot_path TEXT');
|
||||
|
||||
const checkIndexes = db.prepare("PRAGMA index_list('checks')").all();
|
||||
for (const idx of checkIndexes) {
|
||||
if (idx.unique) {
|
||||
// Skip auto indexes created from PRIMARY KEY/UNIQUE constraints; SQLite refuses to drop them
|
||||
if (idx.origin !== 'c' || (idx.name && idx.name.startsWith('sqlite_autoindex'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const info = db.prepare(`PRAGMA index_info('${idx.name}')`).all();
|
||||
const columns = info.map(i => i.name).join(',');
|
||||
if (columns === 'post_id,profile_number' || columns === 'profile_number,post_id') {
|
||||
db.exec(`DROP INDEX IF EXISTS "${idx.name}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const profileStateRow = db.prepare('SELECT profile_number FROM profile_state WHERE id = 1').get();
|
||||
if (!profileStateRow) {
|
||||
db.prepare('INSERT INTO profile_state (id, profile_number) VALUES (1, 1)').run();
|
||||
}
|
||||
|
||||
function mapPostRow(post) {
|
||||
if (!post) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const checks = db.prepare('SELECT id, profile_number, checked_at FROM checks WHERE post_id = ? ORDER BY checked_at ASC').all(post.id);
|
||||
const requiredProfiles = getRequiredProfiles(post.target_count);
|
||||
const { statuses, completedChecks } = buildProfileStatuses(requiredProfiles, checks);
|
||||
const checkedCount = statuses.filter(status => status.status === 'done').length;
|
||||
const screenshotFile = post.screenshot_path ? path.join(screenshotDir, post.screenshot_path) : null;
|
||||
const screenshotPath = screenshotFile && fs.existsSync(screenshotFile)
|
||||
? `/api/posts/${post.id}/screenshot`
|
||||
: null;
|
||||
|
||||
if (post.checked_count !== checkedCount) {
|
||||
db.prepare('UPDATE posts SET checked_count = ? WHERE id = ?').run(checkedCount, post.id);
|
||||
}
|
||||
|
||||
if (post.target_count !== requiredProfiles.length) {
|
||||
db.prepare('UPDATE posts SET target_count = ? WHERE id = ?').run(requiredProfiles.length, post.id);
|
||||
}
|
||||
|
||||
const nextRequired = statuses.find(status => status.status === 'available');
|
||||
|
||||
return {
|
||||
...post,
|
||||
target_count: requiredProfiles.length,
|
||||
checked_count: checkedCount,
|
||||
checks: completedChecks,
|
||||
is_complete: checkedCount >= requiredProfiles.length,
|
||||
screenshot_path: screenshotPath,
|
||||
required_profiles: requiredProfiles,
|
||||
profile_statuses: statuses,
|
||||
next_required_profile: nextRequired ? nextRequired.profile_number : null
|
||||
};
|
||||
}
|
||||
|
||||
// Get all posts
|
||||
app.get('/api/posts', (req, res) => {
|
||||
try {
|
||||
const posts = db.prepare(`
|
||||
SELECT *
|
||||
FROM posts
|
||||
ORDER BY created_at DESC
|
||||
`).all();
|
||||
|
||||
res.json(posts.map(mapPostRow));
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get post by URL
|
||||
app.get('/api/posts/by-url', (req, res) => {
|
||||
try {
|
||||
const { url } = req.query;
|
||||
if (!url) {
|
||||
return res.status(400).json({ error: 'URL parameter required' });
|
||||
}
|
||||
|
||||
const post = db.prepare('SELECT * FROM posts WHERE url = ?').get(url);
|
||||
|
||||
if (!post) {
|
||||
return res.json(null);
|
||||
}
|
||||
|
||||
res.json(mapPostRow(post));
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/profile-state', (req, res) => {
|
||||
try {
|
||||
const row = db.prepare('SELECT profile_number FROM profile_state WHERE id = 1').get();
|
||||
res.json({ profile_number: row ? row.profile_number : 1 });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/profile-state', (req, res) => {
|
||||
try {
|
||||
const { profile_number } = req.body;
|
||||
if (typeof profile_number === 'undefined') {
|
||||
return res.status(400).json({ error: 'profile_number is required' });
|
||||
}
|
||||
|
||||
const parsed = parseInt(profile_number, 10);
|
||||
if (Number.isNaN(parsed) || parsed < 1 || parsed > 5) {
|
||||
return res.status(400).json({ error: 'profile_number must be between 1 and 5' });
|
||||
}
|
||||
|
||||
db.prepare('UPDATE profile_state SET profile_number = ? WHERE id = 1').run(parsed);
|
||||
|
||||
res.json({ profile_number: parsed });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/posts/:postId/screenshot', (req, res) => {
|
||||
try {
|
||||
const { postId } = req.params;
|
||||
const { imageData } = req.body;
|
||||
|
||||
const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
|
||||
if (!post) {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
|
||||
if (!imageData || typeof imageData !== 'string') {
|
||||
return res.status(400).json({ error: 'imageData is required' });
|
||||
}
|
||||
|
||||
const match = imageData.match(/^data:(image\/\w+);base64,(.+)$/);
|
||||
if (!match) {
|
||||
return res.status(400).json({ error: 'Invalid image data format' });
|
||||
}
|
||||
|
||||
const mimeType = match[1];
|
||||
const base64 = match[2];
|
||||
const extension = mimeType === 'image/jpeg' ? 'jpg' : 'png';
|
||||
const buffer = Buffer.from(base64, 'base64');
|
||||
const fileName = `${postId}.${extension}`;
|
||||
const filePath = path.join(screenshotDir, fileName);
|
||||
|
||||
if (post.screenshot_path && post.screenshot_path !== fileName) {
|
||||
const existingPath = path.join(screenshotDir, post.screenshot_path);
|
||||
if (fs.existsSync(existingPath)) {
|
||||
try {
|
||||
fs.unlinkSync(existingPath);
|
||||
} catch (error) {
|
||||
console.warn('Failed to remove previous screenshot:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, buffer);
|
||||
db.prepare('UPDATE posts SET screenshot_path = ? WHERE id = ?').run(fileName, postId);
|
||||
|
||||
const updatedPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
|
||||
res.json(mapPostRow(updatedPost));
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/posts/:postId/screenshot', (req, res) => {
|
||||
try {
|
||||
const { postId } = req.params;
|
||||
const post = db.prepare('SELECT screenshot_path FROM posts WHERE id = ?').get(postId);
|
||||
|
||||
if (!post || !post.screenshot_path) {
|
||||
return res.status(404).json({ error: 'Screenshot not found' });
|
||||
}
|
||||
|
||||
const filePath = path.join(screenshotDir, post.screenshot_path);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return res.status(404).json({ error: 'Screenshot not found' });
|
||||
}
|
||||
|
||||
res.set('Cache-Control', 'no-store');
|
||||
res.sendFile(filePath);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Create new post
|
||||
app.post('/api/posts', (req, res) => {
|
||||
try {
|
||||
const { url, title, target_count } = req.body;
|
||||
|
||||
const validatedTargetCount = validateTargetCount(target_count);
|
||||
|
||||
if (!url || !validatedTargetCount) {
|
||||
return res.status(400).json({ error: 'URL and target_count are required (1-5)' });
|
||||
}
|
||||
|
||||
const id = uuidv4();
|
||||
|
||||
const stmt = db.prepare('INSERT INTO posts (id, url, title, target_count, checked_count, screenshot_path) VALUES (?, ?, ?, ?, 0, NULL)');
|
||||
stmt.run(id, url, title || '', validatedTargetCount);
|
||||
|
||||
const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(id);
|
||||
|
||||
res.json(mapPostRow(post));
|
||||
} catch (error) {
|
||||
if (error.message.includes('UNIQUE constraint failed')) {
|
||||
res.status(409).json({ error: 'Post with this URL already exists' });
|
||||
} else {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/posts/:postId', (req, res) => {
|
||||
try {
|
||||
const { postId } = req.params;
|
||||
const { target_count, title } = req.body || {};
|
||||
|
||||
const updates = [];
|
||||
const params = [];
|
||||
|
||||
if (typeof target_count !== 'undefined') {
|
||||
const validatedTargetCount = validateTargetCount(target_count);
|
||||
if (!validatedTargetCount) {
|
||||
return res.status(400).json({ error: 'target_count must be between 1 and 5' });
|
||||
}
|
||||
updates.push('target_count = ?');
|
||||
params.push(validatedTargetCount);
|
||||
}
|
||||
|
||||
if (typeof title !== 'undefined') {
|
||||
updates.push('title = ?');
|
||||
params.push(title || '');
|
||||
}
|
||||
|
||||
if (!updates.length) {
|
||||
return res.status(400).json({ error: 'No valid fields to update' });
|
||||
}
|
||||
|
||||
params.push(postId);
|
||||
|
||||
const stmt = db.prepare(`UPDATE posts SET ${updates.join(', ')} WHERE id = ?`);
|
||||
const result = stmt.run(...params);
|
||||
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
|
||||
recalcCheckedCount(postId);
|
||||
|
||||
const updatedPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
|
||||
res.json(mapPostRow(updatedPost));
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Check a post for a profile
|
||||
app.post('/api/posts/:postId/check', (req, res) => {
|
||||
try {
|
||||
const { postId } = req.params;
|
||||
const { profile_number } = req.body;
|
||||
|
||||
const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
|
||||
if (!post) {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
|
||||
const requiredProfiles = getRequiredProfiles(post.target_count);
|
||||
if (post.target_count !== requiredProfiles.length) {
|
||||
db.prepare('UPDATE posts SET target_count = ? WHERE id = ?').run(requiredProfiles.length, postId);
|
||||
post.target_count = requiredProfiles.length;
|
||||
}
|
||||
|
||||
let profileValue = sanitizeProfileNumber(profile_number);
|
||||
if (!profileValue) {
|
||||
const stateRow = db.prepare('SELECT profile_number FROM profile_state WHERE id = 1').get();
|
||||
profileValue = sanitizeProfileNumber(stateRow ? stateRow.profile_number : null) || requiredProfiles[0];
|
||||
}
|
||||
|
||||
if (!requiredProfiles.includes(profileValue)) {
|
||||
return res.status(409).json({ error: 'Dieses Profil ist für diesen Beitrag nicht erforderlich.' });
|
||||
}
|
||||
|
||||
const existingCheck = db.prepare(
|
||||
'SELECT id FROM checks WHERE post_id = ? AND profile_number = ?'
|
||||
).get(postId, profileValue);
|
||||
|
||||
if (existingCheck) {
|
||||
const existingPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
|
||||
return res.json(mapPostRow(existingPost));
|
||||
}
|
||||
|
||||
if (requiredProfiles.length > 0) {
|
||||
const prerequisiteProfiles = requiredProfiles.slice(0, requiredProfiles.indexOf(profileValue));
|
||||
if (prerequisiteProfiles.length) {
|
||||
const completedRows = db.prepare('SELECT profile_number FROM checks WHERE post_id = ?').all(postId);
|
||||
const completedSet = new Set(
|
||||
completedRows
|
||||
.map(row => sanitizeProfileNumber(row.profile_number))
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
const missingPrerequisites = prerequisiteProfiles.filter(num => !completedSet.has(num));
|
||||
if (missingPrerequisites.length) {
|
||||
return res.status(409).json({
|
||||
error: 'Vorherige Profile müssen zuerst bestätigen.',
|
||||
missing_profiles: missingPrerequisites
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const insertStmt = db.prepare('INSERT INTO checks (post_id, profile_number) VALUES (?, ?)');
|
||||
insertStmt.run(postId, profileValue);
|
||||
recalcCheckedCount(postId);
|
||||
|
||||
const updatedPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
|
||||
|
||||
res.json(mapPostRow(updatedPost));
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Check by URL (for web interface auto-check)
|
||||
app.post('/api/check-by-url', (req, res) => {
|
||||
try {
|
||||
const { url, profile_number } = req.body;
|
||||
|
||||
if (!url) {
|
||||
return res.status(400).json({ error: 'URL is required' });
|
||||
}
|
||||
|
||||
const post = db.prepare('SELECT * FROM posts WHERE url = ?').get(url);
|
||||
if (!post) {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
|
||||
const requiredProfiles = getRequiredProfiles(post.target_count);
|
||||
if (post.target_count !== requiredProfiles.length) {
|
||||
db.prepare('UPDATE posts SET target_count = ? WHERE id = ?').run(requiredProfiles.length, post.id);
|
||||
post.target_count = requiredProfiles.length;
|
||||
}
|
||||
|
||||
let profileValue = sanitizeProfileNumber(profile_number);
|
||||
if (!profileValue) {
|
||||
const stateRow = db.prepare('SELECT profile_number FROM profile_state WHERE id = 1').get();
|
||||
profileValue = sanitizeProfileNumber(stateRow ? stateRow.profile_number : null) || requiredProfiles[0];
|
||||
}
|
||||
|
||||
if (!requiredProfiles.includes(profileValue)) {
|
||||
return res.status(409).json({ error: 'Dieses Profil ist für diesen Beitrag nicht erforderlich.' });
|
||||
}
|
||||
|
||||
const existingCheck = db.prepare(
|
||||
'SELECT id FROM checks WHERE post_id = ? AND profile_number = ?'
|
||||
).get(post.id, profileValue);
|
||||
|
||||
if (existingCheck) {
|
||||
const updatedPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(post.id);
|
||||
return res.json(mapPostRow(updatedPost));
|
||||
}
|
||||
|
||||
if (requiredProfiles.length > 0) {
|
||||
const prerequisiteProfiles = requiredProfiles.slice(0, requiredProfiles.indexOf(profileValue));
|
||||
if (prerequisiteProfiles.length) {
|
||||
const completedRows = db.prepare('SELECT profile_number FROM checks WHERE post_id = ?').all(post.id);
|
||||
const completedSet = new Set(
|
||||
completedRows
|
||||
.map(row => sanitizeProfileNumber(row.profile_number))
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
const missingPrerequisites = prerequisiteProfiles.filter(num => !completedSet.has(num));
|
||||
if (missingPrerequisites.length) {
|
||||
return res.status(409).json({
|
||||
error: 'Vorherige Profile müssen zuerst bestätigen.',
|
||||
missing_profiles: missingPrerequisites
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.prepare('INSERT INTO checks (post_id, profile_number) VALUES (?, ?)').run(post.id, profileValue);
|
||||
recalcCheckedCount(post.id);
|
||||
|
||||
const updatedPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(post.id);
|
||||
|
||||
res.json(mapPostRow(updatedPost));
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete post
|
||||
app.delete('/api/posts/:postId', (req, res) => {
|
||||
try {
|
||||
const { postId } = req.params;
|
||||
|
||||
const post = db.prepare('SELECT screenshot_path FROM posts WHERE id = ?').get(postId);
|
||||
|
||||
db.prepare('DELETE FROM checks WHERE post_id = ?').run(postId);
|
||||
const result = db.prepare('DELETE FROM posts WHERE id = ?').run(postId);
|
||||
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({ error: 'Post not found' });
|
||||
}
|
||||
|
||||
if (post && post.screenshot_path) {
|
||||
const filePath = path.join(screenshotDir, post.screenshot_path);
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
} catch (error) {
|
||||
console.warn('Failed to remove screenshot:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user