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
|
||||
|
||||
# === Serve-Phase: nginx + ImageMagick fuer Thumbnails ===
|
||||
FROM nginx:alpine
|
||||
# === Serve-Phase: node.js backend + ImageMagick ===
|
||||
FROM node:20-alpine
|
||||
|
||||
# Nginx als Root laufen lassen, um Berechtigungsprobleme mit Nextcloud-Dateien zu vermeiden
|
||||
RUN sed -i 's/user nginx;/user root;/g' /etc/nginx/nginx.conf
|
||||
RUN apk add --no-cache imagemagick docker-cli
|
||||
|
||||
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 --from=builder /site/_site /usr/share/nginx/html
|
||||
COPY backend ./backend
|
||||
COPY --from=builder /site/_site ./public
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Leere Bildverzeichnisse anlegen (werden durch Volumes ueberschrieben)
|
||||
RUN mkdir -p /usr/share/nginx/html/images/fulls \
|
||||
/usr/share/nginx/html/images/thumbs
|
||||
# Verzeichnisse anlegen
|
||||
RUN mkdir -p /app/public/images/fulls /app/public/images/thumbs /app/data
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 8080
|
||||
|
||||
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',
|
||||
cache: false,
|
||||
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) {
|
||||
if (f.type !== 'file') return false;
|
||||
var ext = f.name.split('.').pop().toLowerCase();
|
||||
@@ -167,7 +172,7 @@
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -225,7 +230,8 @@
|
||||
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,
|
||||
onPopupClose: function () {
|
||||
@@ -273,7 +279,26 @@
|
||||
return template;
|
||||
}
|
||||
|
||||
// Galerie laden
|
||||
loadGallery();
|
||||
// Login Handling
|
||||
$.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);
|
||||
|
||||
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
|
||||
container_name: photography-website
|
||||
ports:
|
||||
- "8090:80"
|
||||
- "8090:8080"
|
||||
volumes:
|
||||
# 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
|
||||
- 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
|
||||
|
||||
volumes:
|
||||
thumbs:
|
||||
database:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
FULLS_DIR="/usr/share/nginx/html/images/fulls"
|
||||
THUMBS_DIR="/usr/share/nginx/html/images/thumbs"
|
||||
FULLS_DIR="/app/public/images/fulls"
|
||||
THUMBS_DIR="/app/public/images/thumbs"
|
||||
|
||||
generate_thumbs() {
|
||||
mkdir -p "$THUMBS_DIR"
|
||||
@@ -35,5 +35,5 @@ echo "=== Thumbnail-Generierung abgeschlossen ==="
|
||||
generate_thumbs
|
||||
done) &
|
||||
|
||||
# nginx starten
|
||||
nginx -g 'daemon off;'
|
||||
# Node Server starten
|
||||
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>
|
||||
<nav>
|
||||
<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>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -108,4 +109,28 @@ layout: default
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
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"
|
||||
},
|
||||
"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": {
|
||||
"del": "^4.1.1",
|
||||
"gulp": "^4.0.2",
|
||||
|
||||
Reference in New Issue
Block a user