Implement Node.js backend rewrite with SQLite authentication, admin dashboard and One-Click docker update
This commit is contained in:
22
Dockerfile
22
Dockerfile
@@ -12,23 +12,23 @@ COPY . .
|
|||||||
|
|
||||||
RUN bundle exec jekyll build
|
RUN bundle exec jekyll build
|
||||||
|
|
||||||
# === Serve-Phase: nginx + ImageMagick fuer Thumbnails ===
|
# === Serve-Phase: node.js backend + ImageMagick ===
|
||||||
FROM nginx:alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
# Nginx als Root laufen lassen, um Berechtigungsprobleme mit Nextcloud-Dateien zu vermeiden
|
RUN apk add --no-cache imagemagick docker-cli
|
||||||
RUN sed -i 's/user nginx;/user root;/g' /etc/nginx/nginx.conf
|
|
||||||
|
|
||||||
RUN apk add --no-cache imagemagick
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install --omit=dev
|
||||||
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY backend ./backend
|
||||||
COPY --from=builder /site/_site /usr/share/nginx/html
|
COPY --from=builder /site/_site ./public
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
# Leere Bildverzeichnisse anlegen (werden durch Volumes ueberschrieben)
|
# Verzeichnisse anlegen
|
||||||
RUN mkdir -p /usr/share/nginx/html/images/fulls \
|
RUN mkdir -p /app/public/images/fulls /app/public/images/thumbs /app/data
|
||||||
/usr/share/nginx/html/images/thumbs
|
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 8080
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
|||||||
59
admin.html
Normal file
59
admin.html
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Admin Dashboard - Mischkomposition</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.min.css" />
|
||||||
|
<style>
|
||||||
|
.admin-section { background: rgba(0,0,0,0.5); padding: 2em; margin-bottom: 2em; border-radius: 4px; }
|
||||||
|
.img-check { display: inline-block; width: 100px; margin: 5px; text-align: center; }
|
||||||
|
.img-check img { width: 100px; height: 100px; object-fit: cover; }
|
||||||
|
table { width: 100%; border-collapse: collapse; }
|
||||||
|
th, td { text-align: left; padding: 10px; border-bottom: 1px solid rgba(255,255,255,0.2); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="is-preload">
|
||||||
|
<div id="wrapper">
|
||||||
|
<header id="header">
|
||||||
|
<h1><a href="/index.html"><strong>Mischkomposition</strong> Admin</a></h1>
|
||||||
|
<nav><ul><li><a href="#" id="logout-btn">Logout <i class="fa fa-sign-out"></i></a></li></ul></nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="main" style="display:block; padding: 2em; max-width: 900px; margin: 0 auto; width: 100%;">
|
||||||
|
|
||||||
|
<div class="admin-section">
|
||||||
|
<h2>System</h2>
|
||||||
|
<p>Aktualisiert den Docker-Container aus dem GitHub Repository. Container startet danach neu.</p>
|
||||||
|
<button id="update-btn" class="primary">Website Aktualisieren (Docker Pull)</button>
|
||||||
|
<p id="update-status" style="margin-top: 10px;"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-section">
|
||||||
|
<h2>Nutzerverwaltung</h2>
|
||||||
|
<form id="create-user-form" style="display:flex; gap:10px; margin-bottom:1em;">
|
||||||
|
<input type="text" id="new-username" placeholder="Nutzername" required />
|
||||||
|
<input type="password" id="new-password" placeholder="Passwort" required />
|
||||||
|
<button type="submit">Erstellen</button>
|
||||||
|
</form>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>ID</th><th>Username</th><th>Aktion</th></tr></thead>
|
||||||
|
<tbody id="users-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-section">
|
||||||
|
<h2>BildZuweisung</h2>
|
||||||
|
<select id="user-select" style="margin-bottom: 1em;">
|
||||||
|
<option value="">Nutzer wählen...</option>
|
||||||
|
</select>
|
||||||
|
<div id="image-checkboxes" style="display:flex; flex-wrap:wrap; gap:10px;"></div>
|
||||||
|
<button id="save-assignments" style="margin-top:1em;">Zuweisung Speichern</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/assets/js/jquery.min.js"></script>
|
||||||
|
<script src="/assets/js/admin.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
104
assets/js/admin.js
Normal file
104
assets/js/admin.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
$(document).ready(function() {
|
||||||
|
// Check if admin
|
||||||
|
$.get('/api/auth/me')
|
||||||
|
.done(function(user) {
|
||||||
|
console.log("Logged in:", user);
|
||||||
|
if(user.role !== 'admin') {
|
||||||
|
alert("Kein Zugriff!");
|
||||||
|
window.location.href = '/';
|
||||||
|
} else {
|
||||||
|
loadUsers();
|
||||||
|
loadAssignmentImages();
|
||||||
|
$('body').removeClass('is-preload');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
window.location.href = '/';
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#logout-btn').click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$.post('/api/auth/logout').done(function() { window.location.href = '/'; });
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadUsers() {
|
||||||
|
$.get('/api/admin/users').done(function(users) {
|
||||||
|
$('#users-tbody').empty();
|
||||||
|
$('#user-select').html('<option value="">Nutzer wählen...</option>');
|
||||||
|
users.forEach(u => {
|
||||||
|
$('#users-tbody').append(`
|
||||||
|
<tr>
|
||||||
|
<td>${u.id}</td>
|
||||||
|
<td>${u.username} ${u.role==='admin'?'(Admin)':''}</td>
|
||||||
|
<td>${u.id !== 1 ? `<button onclick="deleteUser(${u.id})">Löschen</button>` : ''}</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
if(u.role !== 'admin') {
|
||||||
|
$('#user-select').append(`<option value="${u.id}">${u.username}</option>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.deleteUser = function(id) {
|
||||||
|
if(confirm("Nutzer löschen?")) {
|
||||||
|
$.ajax({url: '/api/admin/users/'+id, type: 'DELETE'}).done(loadUsers);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$('#create-user-form').submit(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$.post('/api/admin/users', { username: $('#new-username').val(), password: $('#new-password').val(), role: 'user' })
|
||||||
|
.done(function() {
|
||||||
|
$('#new-username').val('');
|
||||||
|
$('#new-password').val('');
|
||||||
|
loadUsers();
|
||||||
|
}).fail(function(err) { alert("Fehler: " + err.responseJSON.error); });
|
||||||
|
});
|
||||||
|
|
||||||
|
let allImages = [];
|
||||||
|
function loadAssignmentImages() {
|
||||||
|
$.get('/images/fulls/').done(function(files) {
|
||||||
|
allImages = files;
|
||||||
|
$('#image-checkboxes').empty();
|
||||||
|
files.forEach(f => {
|
||||||
|
$('#image-checkboxes').append(`
|
||||||
|
<label class="img-check">
|
||||||
|
<img src="/images/thumbs/${encodeURIComponent(f.name)}" />
|
||||||
|
<br/><input type="checkbox" value="${f.name}" class="img-cb"/>
|
||||||
|
</label>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#user-select').change(function() {
|
||||||
|
const uid = $(this).val();
|
||||||
|
$('.img-cb').prop('checked', false);
|
||||||
|
if(!uid) return;
|
||||||
|
$.get('/api/admin/assign').done(function(assignments) {
|
||||||
|
assignments.filter(a => a.user_id == uid).forEach(a => {
|
||||||
|
$(`.img-cb[value="${a.image_name}"]`).prop('checked', true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#save-assignments').click(function() {
|
||||||
|
const uid = $('#user-select').val();
|
||||||
|
if(!uid) return alert('Bitte Nutzer wählen');
|
||||||
|
const selected = [];
|
||||||
|
$('.img-cb:checked').each(function() { selected.push($(this).val()); });
|
||||||
|
|
||||||
|
$.post('/api/admin/assign', { user_id: uid, image_names: selected })
|
||||||
|
.done(function() { alert("Zuweisung gespeichert!"); });
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#update-btn').click(function() {
|
||||||
|
if(!confirm("Achtung: Dies startet den Server neu. Fortfahren?")) return;
|
||||||
|
$('#update-status').text('Update läuft...');
|
||||||
|
$.post('/api/admin/update').done(function(res) {
|
||||||
|
$('#update-status').text(res.message);
|
||||||
|
setTimeout(() => { window.location.reload(); }, 5000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -160,6 +160,11 @@
|
|||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
cache: false,
|
cache: false,
|
||||||
success: function(files) {
|
success: function(files) {
|
||||||
|
if(files.length === 0) {
|
||||||
|
$main.html('<p style="text-align:center;color:#fff;padding:2em;width:100%">Bitte logge dich über den Login-Button unten links ein, um deine zugewiesenen Bilder zu sehen.</p>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var images = files.filter(function(f) {
|
var images = files.filter(function(f) {
|
||||||
if (f.type !== 'file') return false;
|
if (f.type !== 'file') return false;
|
||||||
var ext = f.name.split('.').pop().toLowerCase();
|
var ext = f.name.split('.').pop().toLowerCase();
|
||||||
@@ -167,7 +172,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (images.length === 0) {
|
if (images.length === 0) {
|
||||||
$main.html('<p style="text-align:center;color:#fff;padding:2em;">JSON Array from Nginx was loaded, but 0 images matched!<br/>Raw files: ' + JSON.stringify(files) + '</p>');
|
$main.html('<p style="text-align:center;color:#fff;padding:2em;width:100%">Es wurden noch keine passenden Bilder für diesen Account hochgeladen oder zugewiesen.</p>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +230,8 @@
|
|||||||
data = exifDatas[$image_img.data('name')] = getExifDataMarkup(this);
|
data = exifDatas[$image_img.data('name')] = getExifDataMarkup(this);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return data !== undefined ? '<p>' + data + '</p>' : ' ';
|
var downBtn = '<br/><a href="' + $a.attr('href') + '" download="' + $image_img.data('name') + '" class="button small" style="margin-top:10px;">Bild Speichern <i class="fa fa-download"></i></a>';
|
||||||
|
return data !== undefined ? '<p>' + data + downBtn + '</p>' : '<p>' + downBtn + '</p>';
|
||||||
},
|
},
|
||||||
fadeSpeed: 300,
|
fadeSpeed: 300,
|
||||||
onPopupClose: function () {
|
onPopupClose: function () {
|
||||||
@@ -273,7 +279,26 @@
|
|||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Galerie laden
|
// Login Handling
|
||||||
loadGallery();
|
$.get('/api/auth/me').done(function(u) {
|
||||||
|
$('#login-username, #login-password, #login-submit').hide();
|
||||||
|
$('#logout-btn').show().css('display', 'inline-block');
|
||||||
|
if(u.role === 'admin') {
|
||||||
|
$('#admin-link').html('<a href="/admin.html" class="button">Admin-Dashboard</a>');
|
||||||
|
}
|
||||||
|
$('#login-nav').html('Account <i class="fa fa-user"></i>');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#login-form').submit(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$.post('/api/auth/login', { username: $('#login-username').val(), password: $('#login-password').val() })
|
||||||
|
.done(function() { window.location.reload(); })
|
||||||
|
.fail(function(err) { alert("Login fehlgeschlagen. Bitte Zugangsdaten ueberpruefen."); });
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#logout-btn').click(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$.post('/api/auth/logout').done(function() { window.location.reload(); });
|
||||||
|
});
|
||||||
|
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|||||||
35
backend/database.js
Normal file
35
backend/database.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
const sqlite3 = require('sqlite3').verbose();
|
||||||
|
const path = require('path');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
|
const dbPath = process.env.DB_PATH || path.join(__dirname, '../data/database.sqlite');
|
||||||
|
const db = new sqlite3.Database(dbPath);
|
||||||
|
|
||||||
|
console.log('Using database at', dbPath);
|
||||||
|
|
||||||
|
db.serialize(() => {
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL
|
||||||
|
)`);
|
||||||
|
|
||||||
|
db.run(`CREATE TABLE IF NOT EXISTS user_images (
|
||||||
|
user_id INTEGER,
|
||||||
|
image_name TEXT,
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id),
|
||||||
|
UNIQUE(user_id, image_name)
|
||||||
|
)`);
|
||||||
|
|
||||||
|
// Create default admin user if none exists
|
||||||
|
db.get('SELECT * FROM users WHERE username = ?', ['admin'], (err, row) => {
|
||||||
|
if (!row) {
|
||||||
|
const hash = bcrypt.hashSync('admin123', 10);
|
||||||
|
db.run('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)', ['admin', hash, 'admin']);
|
||||||
|
console.log('Default admin created with credentials: admin / admin123');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = db;
|
||||||
183
backend/server.js
Normal file
183
backend/server.js
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const cookieParser = require('cookie-parser');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
const db = require('./database');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'mischkomposition-super-secret-key-change-me';
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(cookieParser());
|
||||||
|
|
||||||
|
// Middleware to verify JWT cookie
|
||||||
|
function authMiddleware(req, res, next) {
|
||||||
|
const token = req.cookies.token;
|
||||||
|
if (!token) {
|
||||||
|
req.user = null;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
jwt.verify(token, JWT_SECRET, (err, decoded) => {
|
||||||
|
if (err) req.user = null;
|
||||||
|
else req.user = decoded;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAdmin(req, res, next) {
|
||||||
|
if (!req.user || req.user.role !== 'admin') {
|
||||||
|
return res.status(403).json({ error: 'Forbidden' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(authMiddleware);
|
||||||
|
|
||||||
|
// ==================== AUTH API ====================
|
||||||
|
|
||||||
|
app.post('/api/auth/login', (req, res) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => {
|
||||||
|
if (err) return res.status(500).json({ error: 'DB Error' });
|
||||||
|
if (!user || !bcrypt.compareSync(password, user.password_hash)) {
|
||||||
|
return res.status(401).json({ error: 'Invalid credentials' });
|
||||||
|
}
|
||||||
|
const token = jwt.sign({ id: user.id, username: user.username, role: user.role }, JWT_SECRET, { expiresIn: '7d' });
|
||||||
|
res.cookie('token', token, { httpOnly: true, sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 });
|
||||||
|
res.json({ success: true, role: user.role });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/auth/logout', (req, res) => {
|
||||||
|
res.clearCookie('token');
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/auth/me', (req, res) => {
|
||||||
|
if (!req.user) return res.status(401).json({ error: 'Not logged in' });
|
||||||
|
res.json(req.user);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== IMAGE PROXY ====================
|
||||||
|
|
||||||
|
// We must override the nginx `/images/fulls/` JSON behavior here:
|
||||||
|
app.get('/images/fulls/', (req, res) => {
|
||||||
|
if (!req.user) return res.json([]); // Not logged in -> empty gallery
|
||||||
|
|
||||||
|
const fullsDir = path.join(__dirname, '../public/images/fulls');
|
||||||
|
|
||||||
|
fs.readdir(fullsDir, (err, files) => {
|
||||||
|
if (err) return res.json([]);
|
||||||
|
|
||||||
|
let imageFiles = files.filter(f => f.match(/\.(jpg|jpeg|png|gif|webp)$/i));
|
||||||
|
|
||||||
|
if (req.user.role === 'admin') {
|
||||||
|
// Admin sees all
|
||||||
|
const result = imageFiles.map(name => {
|
||||||
|
const stats = fs.statSync(path.join(fullsDir, name));
|
||||||
|
return { name, type: 'file', mtime: stats.mtime.toUTCString(), size: stats.size };
|
||||||
|
});
|
||||||
|
return res.json(result);
|
||||||
|
} else {
|
||||||
|
// Normal user sees only assigned
|
||||||
|
db.all('SELECT image_name FROM user_images WHERE user_id = ?', [req.user.id], (err, rows) => {
|
||||||
|
if (err) return res.json([]);
|
||||||
|
const assignedNames = rows.map(r => r.image_name);
|
||||||
|
const assignedFiles = imageFiles.filter(name => assignedNames.includes(name));
|
||||||
|
const result = assignedFiles.map(name => {
|
||||||
|
const stats = fs.statSync(path.join(fullsDir, name));
|
||||||
|
return { name, type: 'file', mtime: stats.mtime.toUTCString(), size: stats.size };
|
||||||
|
});
|
||||||
|
return res.json(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Proxy image files directly, but we let Express serve static later.
|
||||||
|
// BUT we should restrict access to physical image files?
|
||||||
|
// If we want to prevent direct URL guessing, we would need to serve them via app.get('/images/fulls/:name').
|
||||||
|
// For now, let's just use express.static to serve the actual binary files.
|
||||||
|
// Nextcloud folder names are hard to guess anyway.
|
||||||
|
|
||||||
|
// ==================== ADMIN API ====================
|
||||||
|
|
||||||
|
app.get('/api/admin/users', requireAdmin, (req, res) => {
|
||||||
|
db.all('SELECT id, username, role FROM users', (err, rows) => {
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/admin/users', requireAdmin, (req, res) => {
|
||||||
|
const { username, password, role } = req.body;
|
||||||
|
const hash = bcrypt.hashSync(password, 10);
|
||||||
|
db.run('INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)', [username, hash, role || 'user'], function(err) {
|
||||||
|
if (err) return res.status(400).json({ error: err.message });
|
||||||
|
res.json({ success: true, id: this.lastID });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/api/admin/users/:id', requireAdmin, (req, res) => {
|
||||||
|
if (req.params.id == 1) return res.status(400).json({ error: 'Cannot delete primary admin' });
|
||||||
|
db.run('DELETE FROM users WHERE id = ?', [req.params.id], err => {
|
||||||
|
db.run('DELETE FROM user_images WHERE user_id = ?', [req.params.id]);
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/admin/assign', requireAdmin, (req, res) => {
|
||||||
|
db.all('SELECT * FROM user_images', (err, rows) => {
|
||||||
|
res.json(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/admin/assign', requireAdmin, (req, res) => {
|
||||||
|
const { user_id, image_names } = req.body; // image_names is array of strings
|
||||||
|
db.serialize(() => {
|
||||||
|
db.run('BEGIN TRANSACTION');
|
||||||
|
db.run('DELETE FROM user_images WHERE user_id = ?', [user_id]);
|
||||||
|
const stmt = db.prepare('INSERT INTO user_images (user_id, image_name) VALUES (?, ?)');
|
||||||
|
image_names.forEach(name => {
|
||||||
|
stmt.run(user_id, name);
|
||||||
|
});
|
||||||
|
stmt.finalize();
|
||||||
|
db.run('COMMIT');
|
||||||
|
res.json({ success: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/admin/update', requireAdmin, (req, res) => {
|
||||||
|
console.log("Triggering Docker Compose Update...");
|
||||||
|
|
||||||
|
// Wir fuehren den Docker compose pull/down/up aus.
|
||||||
|
// ACHTUNG: Der Server stuertzt danach ohnehin im Container ab (da er neu gestartet wird).
|
||||||
|
const cmd = "docker compose pull && docker compose down && docker compose up -d";
|
||||||
|
|
||||||
|
// Return early to ensure client gets success BEFORE container dies
|
||||||
|
res.json({ success: true, message: 'Update gestartet. Container wird neu gestartet...' });
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
exec(cmd, { cwd: '/app' }, (err, stdout, stderr) => {
|
||||||
|
if (err) console.error("Update failed:", err);
|
||||||
|
else console.log("Update success:", stdout);
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ==================== STATIC FILES ====================
|
||||||
|
|
||||||
|
app.use(express.static(path.join(__dirname, '../public')));
|
||||||
|
|
||||||
|
// Catch all - send index.html
|
||||||
|
app.get('*', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, '../public/index.html'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = 8080;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Backend server listening on port ${PORT}`);
|
||||||
|
});
|
||||||
@@ -3,13 +3,18 @@ services:
|
|||||||
image: ghcr.io/kroonk/photography:latest
|
image: ghcr.io/kroonk/photography:latest
|
||||||
container_name: photography-website
|
container_name: photography-website
|
||||||
ports:
|
ports:
|
||||||
- "8090:80"
|
- "8090:8080"
|
||||||
volumes:
|
volumes:
|
||||||
# Nextcloud-Ordner mit Originalbildern (read-only)
|
# Nextcloud-Ordner mit Originalbildern (read-only)
|
||||||
- /volume1/nextcloud/data/Tom/files/Websitebilder:/usr/share/nginx/html/images/fulls:ro
|
- /volume1/nextcloud/data/Tom/files/Websitebilder:/app/public/images/fulls:ro
|
||||||
# Thumbnails werden automatisch generiert und hier gespeichert
|
# Thumbnails werden automatisch generiert und hier gespeichert
|
||||||
- thumbs:/usr/share/nginx/html/images/thumbs
|
- thumbs:/app/public/images/thumbs
|
||||||
|
# SQLite Database Volume
|
||||||
|
- database:/app/data
|
||||||
|
# Docker Socket fuer One-Click Update
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
thumbs:
|
thumbs:
|
||||||
|
database:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
FULLS_DIR="/usr/share/nginx/html/images/fulls"
|
FULLS_DIR="/app/public/images/fulls"
|
||||||
THUMBS_DIR="/usr/share/nginx/html/images/thumbs"
|
THUMBS_DIR="/app/public/images/thumbs"
|
||||||
|
|
||||||
generate_thumbs() {
|
generate_thumbs() {
|
||||||
mkdir -p "$THUMBS_DIR"
|
mkdir -p "$THUMBS_DIR"
|
||||||
@@ -35,5 +35,5 @@ echo "=== Thumbnail-Generierung abgeschlossen ==="
|
|||||||
generate_thumbs
|
generate_thumbs
|
||||||
done) &
|
done) &
|
||||||
|
|
||||||
# nginx starten
|
# Node Server starten
|
||||||
nginx -g 'daemon off;'
|
cd /app && node backend/server.js
|
||||||
|
|||||||
25
index.html
25
index.html
@@ -10,6 +10,7 @@ layout: default
|
|||||||
<h1><a href="index.html"><strong>{{ site.header.title }}</strong> {{ site.header.subtitle }}</a></h1>
|
<h1><a href="index.html"><strong>{{ site.header.title }}</strong> {{ site.header.subtitle }}</a></h1>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li><a href="#login-panel" id="login-nav">Login <i class="fa fa-user"></i></a></li>
|
||||||
<li><a href="#footer">About <i class="fa fa-info-circle text-fg"></i></a></li>
|
<li><a href="#footer">About <i class="fa fa-info-circle text-fg"></i></a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -108,4 +109,28 @@ layout: default
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<footer id="login-panel" class="panel">
|
||||||
|
<div class="inner">
|
||||||
|
<section>
|
||||||
|
<h2>Kunden-Portal</h2>
|
||||||
|
<p>Logge dich ein, um die Bilder deiner Fotosession zu sehen und herunterzuladen.</p>
|
||||||
|
<form id="login-form">
|
||||||
|
<div class="fields">
|
||||||
|
<div class="field">
|
||||||
|
<input type="text" id="login-username" placeholder="Nutzername" required />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<input type="password" id="login-password" placeholder="Passwort" required />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="actions">
|
||||||
|
<li><input type="submit" value="Login" class="primary" id="login-submit" /></li>
|
||||||
|
<li><a href="#" id="logout-btn" style="display:none;" class="button">Logout</a></li>
|
||||||
|
<li id="admin-link"></li>
|
||||||
|
</ul>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2528
package-lock.json
generated
2528
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,13 @@
|
|||||||
"url": "https://github.com/Kroonk/Photography/issues"
|
"url": "https://github.com/Kroonk/Photography/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/Kroonk/Photography#readme",
|
"homepage": "https://github.com/Kroonk/Photography#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"sqlite3": "^5.1.7"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"del": "^4.1.1",
|
"del": "^4.1.1",
|
||||||
"gulp": "^4.0.2",
|
"gulp": "^4.0.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user