Improve overall app security by: - using dynamic statements for all sql querries - introducing environment variables for initial admin password - introducing enironment variable for cors address - improving error handling
338 lines
8.4 KiB
HTML
338 lines
8.4 KiB
HTML
<!doctype html>
|
|
<html lang="de">
|
|
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
|
<title>Zeiterfassung</title>
|
|
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" />
|
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
|
|
|
<style>
|
|
/* Toast-Container */
|
|
.toast-container {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
z-index: 9999;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
max-width: 400px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Basis-Toast */
|
|
.toast {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
backdrop-filter: blur(10px);
|
|
pointer-events: all;
|
|
min-width: 320px;
|
|
transition: all 0.3s ease;
|
|
border-left: 4px solid;
|
|
}
|
|
|
|
.toast:hover {
|
|
transform: translateX(-5px);
|
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
/* Toast-Content */
|
|
.toast-content {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex: 1;
|
|
}
|
|
|
|
.toast-icon {
|
|
font-size: 1.25rem;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.toast-message {
|
|
font-size: 0.95rem;
|
|
line-height: 1.4;
|
|
color: #2c3e50;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Close-Button */
|
|
.toast-close {
|
|
background: transparent;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
margin-left: 12px;
|
|
color: rgba(0, 0, 0, 0.4);
|
|
transition: color 0.2s ease;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.toast-close:hover {
|
|
color: rgba(0, 0, 0, 0.7);
|
|
}
|
|
|
|
/* Toast-Typen */
|
|
.toast-error {
|
|
background: linear-gradient(135deg, #fff5f5 0%, #ffe5e5 100%);
|
|
border-left-color: #e53e3e;
|
|
}
|
|
|
|
.toast-error .toast-icon {
|
|
color: #e53e3e;
|
|
}
|
|
|
|
.toast-success {
|
|
background: linear-gradient(135deg, #f0fff4 0%, #e6ffed 100%);
|
|
border-left-color: #38a169;
|
|
}
|
|
|
|
.toast-success .toast-icon {
|
|
color: #38a169;
|
|
}
|
|
|
|
.toast-info {
|
|
background: linear-gradient(135deg, #ebf8ff 0%, #e0f3ff 100%);
|
|
border-left-color: #3182ce;
|
|
}
|
|
|
|
.toast-info .toast-icon {
|
|
color: #3182ce;
|
|
}
|
|
|
|
.toast-warning {
|
|
background: linear-gradient(135deg, #fffaf0 0%, #fff5e6 100%);
|
|
border-left-color: #dd6b20;
|
|
}
|
|
|
|
.toast-warning .toast-icon {
|
|
color: #dd6b20;
|
|
}
|
|
|
|
/* Animationen */
|
|
@keyframes slideIn {
|
|
from {
|
|
transform: translateX(400px);
|
|
opacity: 0;
|
|
}
|
|
|
|
to {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes slideOut {
|
|
from {
|
|
transform: translateX(0);
|
|
opacity: 1;
|
|
}
|
|
|
|
to {
|
|
transform: translateX(400px);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.toast.dismissing {
|
|
animation: slideOut 0.3s ease-in forwards;
|
|
}
|
|
|
|
/* Mobile Anpassungen */
|
|
@media screen and (max-width: 768px) {
|
|
.toast-container {
|
|
top: 10px;
|
|
right: 10px;
|
|
left: 10px;
|
|
max-width: none;
|
|
}
|
|
|
|
.toast {
|
|
min-width: auto;
|
|
width: 100%;
|
|
}
|
|
|
|
.toast-message {
|
|
font-size: 0.9rem;
|
|
}
|
|
}
|
|
|
|
/* Dark Mode Support (optional) */
|
|
@media (prefers-color-scheme: dark) {
|
|
.toast {
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
.toast-message {
|
|
color: #1a202c;
|
|
}
|
|
|
|
.toast-close {
|
|
color: rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.toast-close:hover {
|
|
color: rgba(0, 0, 0, 0.8);
|
|
}
|
|
}
|
|
|
|
body {
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.table-container {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
@media screen and (max-width: 768px) {
|
|
.level {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.level-left,
|
|
.level-right {
|
|
width: 100%;
|
|
}
|
|
|
|
.level-item {
|
|
justify-content: center;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.buttons {
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.button {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
}
|
|
|
|
.fa-spinner {
|
|
animation: fa-spin 1s infinite linear;
|
|
}
|
|
|
|
@keyframes fa-spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div id="elm"></div>
|
|
|
|
<script src="/elm.js"></script>
|
|
<script>
|
|
function getStoredData() {
|
|
try {
|
|
const data = localStorage.getItem("timetracking");
|
|
if (data) {
|
|
return JSON.parse(data);
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to parse stored data:", e);
|
|
}
|
|
return {token: null, isAdmin: false};
|
|
}
|
|
|
|
function saveData(token, isAdmin) {
|
|
try {
|
|
localStorage.setItem(
|
|
"timetracking",
|
|
JSON.stringify({
|
|
token: token,
|
|
isAdmin: isAdmin,
|
|
}),
|
|
);
|
|
} catch (e) {
|
|
console.error("Failed to save data:", e);
|
|
}
|
|
}
|
|
|
|
function clearData() {
|
|
try {
|
|
localStorage.removeItem("timetracking");
|
|
} catch (e) {
|
|
console.error("Failed to clear data:", e);
|
|
}
|
|
}
|
|
|
|
const storedData = getStoredData();
|
|
const app = Elm.Main.init({
|
|
node: document.getElementById("elm"),
|
|
flags: {
|
|
token: storedData.token,
|
|
isAdmin: storedData.isAdmin,
|
|
},
|
|
});
|
|
|
|
app.ports.saveToken.subscribe(function (data) {
|
|
saveData(data.token, data.isAdmin);
|
|
});
|
|
|
|
app.ports.removeToken.subscribe(function () {
|
|
clearData();
|
|
});
|
|
|
|
app.ports.confirmDelete.subscribe(function (message) {
|
|
const confirmed = confirm(message);
|
|
app.ports.confirmDeleteResponse.send(confirmed);
|
|
});
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
function setupBurgerMenu() {
|
|
const burgers = document.querySelectorAll(".navbar-burger");
|
|
|
|
burgers.forEach((burger) => {
|
|
burger.addEventListener("click", () => {
|
|
const target = burger.dataset.target;
|
|
const menu = document.getElementById(target);
|
|
|
|
if (menu) {
|
|
burger.classList.toggle("is-active");
|
|
menu.classList.toggle("is-active");
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
setupBurgerMenu();
|
|
|
|
const observer = new MutationObserver((mutations) => {
|
|
setupBurgerMenu();
|
|
});
|
|
|
|
observer.observe(document.getElementById("elm"), {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
});
|
|
|
|
if (
|
|
"serviceWorker" in navigator &&
|
|
window.location.protocol === "https:"
|
|
) {
|
|
navigator.serviceWorker.register("/sw.js").catch(() => {
|
|
console.log("Service Worker registration failed");
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|