feat: Remove guest token usage, enhance highlight tag management with validation and improved UI, and add security tests.

This commit is contained in:
Kantine Wrapper
2026-03-12 13:13:32 +01:00
parent 877f0f3649
commit 32c1e1e383
14 changed files with 635 additions and 229 deletions

View File

@@ -1,6 +1,6 @@
import { authToken, currentUser, orderMap, userFlags, pollIntervalId, highlightTags, allWeeks, currentWeekNumber, currentYear, displayMode, langMode, setAuthToken, setCurrentUser, setOrderMap, setUserFlags, setPollIntervalId, setHighlightTags, setAllWeeks, setCurrentWeekNumber, setCurrentYear } from './state.js';
import { getISOWeek, getWeekYear, translateDay, escapeHtml, getRelativeTime, isNewer } from './utils.js';
import { API_BASE, GUEST_TOKEN, VENUE_ID, MENU_ID, POLL_INTERVAL_MS, GITHUB_API, INSTALLER_BASE, CLIENT_VERSION, LS } from './constants.js';
import { API_BASE, VENUE_ID, MENU_ID, POLL_INTERVAL_MS, GITHUB_API, INSTALLER_BASE, CLIENT_VERSION, LS } from './constants.js';
import { apiHeaders, githubHeaders } from './api.js';
import { renderVisibleWeeks, updateNextWeekBadge, updateAlarmBell } from './ui_helpers.js';
import { t } from './i18n.js';
@@ -500,7 +500,12 @@ export function saveFlags() {
export async function refreshFlaggedItems() {
if (userFlags.size === 0) return;
const token = authToken || GUEST_TOKEN;
const token = authToken;
if (!token) {
const bellBtn = document.getElementById('alarm-bell');
if (bellBtn) bellBtn.classList.remove('refreshing');
return;
}
// Collect unique dates that have flagged items
const datesToFetch = new Set();
@@ -693,14 +698,26 @@ export function saveHighlightTags() {
}
export function addHighlightTag(tag) {
tag = tag.trim().toLowerCase();
if (tag && !highlightTags.includes(tag)) {
const newTags = [...highlightTags, tag];
setHighlightTags(newTags);
saveHighlightTags();
return true;
if (!tag) return false;
tag = tag.trim();
if (tag.length < 2) {
showToast('Tag muss mindestens 2 Zeichen lang sein.', 'error');
return false;
}
return false;
if (tag.length > 20) {
showToast('Tag darf maximal 20 Zeichen lang sein.', 'error');
return false;
}
// Only allow alphanumeric characters, spaces and common special chars for food
if (!/^[a-zA-Z0-9äöüÄÖÜß\s\-\.]+$/.test(tag)) {
showToast('Ungültige Zeichen im Tag.', 'error');
return false;
}
if (highlightTags.includes(tag)) return false;
const newTags = [...highlightTags, tag];
setHighlightTags(newTags);
saveHighlightTags();
return true;
}
export function removeHighlightTag(tag) {
@@ -711,19 +728,26 @@ export function removeHighlightTag(tag) {
export function renderTagsList() {
const list = document.getElementById('tags-list');
list.innerHTML = '';
if (!list) return;
list.innerHTML = ''; // Clear existing content
highlightTags.forEach(tag => {
const badge = document.createElement('span');
badge.className = 'tag-badge';
badge.innerHTML = `${tag} <span class="tag-remove" data-tag="${tag}" title="Schlagwort entfernen">&times;</span>`;
list.appendChild(badge);
});
list.querySelectorAll('.tag-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
removeHighlightTag(e.target.dataset.tag);
const label = document.createElement('span');
label.textContent = tag;
badge.appendChild(label);
const removeBtn = document.createElement('span');
removeBtn.className = 'tag-remove';
removeBtn.innerHTML = '&times;';
removeBtn.title = t('removeTagTooltip') || 'Entfernen';
removeBtn.onclick = () => {
removeHighlightTag(tag);
renderTagsList();
});
};
badge.appendChild(removeBtn);
list.appendChild(badge);
});
}
@@ -807,7 +831,11 @@ export async function loadMenuDataFromAPI() {
loading.classList.remove('hidden');
const token = authToken || GUEST_TOKEN;
const token = authToken;
if (!token) {
loading.classList.add('hidden');
return;
}
try {
progressModal.classList.remove('hidden');
@@ -974,7 +1002,8 @@ export async function loadMenuDataFromAPI() {
import('./ui_helpers.js').then(uiHelpers => {
uiHelpers.showErrorModal(
'Keine Verbindung',
`Die Menüdaten konnten nicht geladen werden. Möglicherweise besteht keine Verbindung zur API oder zur Bessa-Webseite.<br><br><small style="color:var(--text-secondary)">${escapeHtml(error.message)}</small>`,
'Die Menüdaten konnten nicht geladen werden. Möglicherweise besteht keine Verbindung zur API oder zur Bessa-Webseite.',
error.message,
'Zur Original-Seite',
'https://web.bessa.app/knapp-kantine'
);

View File

@@ -3,20 +3,23 @@
* All fetch calls in the app route through these helpers to ensure
* consistent auth and versioning headers.
*/
import { API_BASE, GUEST_TOKEN, CLIENT_VERSION } from './constants.js';
import { API_BASE, CLIENT_VERSION } from './constants.js';
/**
* Returns request headers for the Bessa REST API.
* @param {string|null} token - Auth token; falls back to GUEST_TOKEN if absent.
* @param {string|null} token - Auth token.
* @returns {Object} HTTP headers for fetch()
*/
export function apiHeaders(token) {
return {
'Authorization': `Token ${token || GUEST_TOKEN}`,
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Client-Version': CLIENT_VERSION
};
if (token) {
headers['Authorization'] = `Token ${token}`;
}
return headers;
}
/**

View File

@@ -7,11 +7,8 @@
/** Base URL for the Bessa REST API (v1). */
export const API_BASE = 'https://api.bessa.app/v1';
/** Guest token for unauthenticated API calls (e.g. browsing the menu). */
export const GUEST_TOKEN = 'c3418725e95a9f90e3645cbc846b4d67c7c66131';
/** The client version injected into every API request header. */
export const CLIENT_VERSION = 'v1.6.19';
export const CLIENT_VERSION = 'v1.7.1';
/** Bessa venue ID for Knapp-Kantine. */
export const VENUE_ID = 591;

View File

@@ -632,56 +632,79 @@ export function removeCountdown() {
setInterval(updateCountdown, 60000);
setTimeout(updateCountdown, 1000);
export function showErrorModal(title, htmlContent, btnText, url) {
export function showErrorModal(title, message, details, btnText, url) {
const modalId = 'error-modal';
let modal = document.getElementById(modalId);
if (modal) modal.remove();
modal = document.createElement('div');
modal.id = modalId;
modal.className = 'modal hidden';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2 style="color: var(--error-color); display: flex; align-items: center; gap: 10px;">
<span class="material-icons-round">signal_wifi_off</span>
${escapeHtml(title)}
</h2>
</div>
<div style="padding: 20px;">
<p style="margin-bottom: 15px; color: var(--text-primary);">${htmlContent}</p>
<div style="margin-top: 20px; display: flex; justify-content: center;">
<button id="btn-error-redirect" style="
background-color: var(--accent-color);
color: white;
padding: 12px 24px;
border-radius: 8px;
border: none;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
width: 100%;
justify-content: center;
transition: transform 0.1s;
">
${escapeHtml(btnText)}
<span class="material-icons-round">open_in_new</span>
</button>
</div>
</div>
</div>
modal.className = 'modal'; // Removed hidden because we are showing it now
const content = document.createElement('div');
content.className = 'modal-content';
const header = document.createElement('div');
header.className = 'modal-header';
const h2 = document.createElement('h2');
h2.style.cssText = 'color: var(--error-color); display: flex; align-items: center; gap: 10px;';
const icon = document.createElement('span');
icon.className = 'material-icons-round';
icon.textContent = 'signal_wifi_off';
h2.appendChild(icon);
const titleSpan = document.createElement('span');
titleSpan.textContent = title;
h2.appendChild(titleSpan);
header.appendChild(h2);
content.appendChild(header);
const body = document.createElement('div');
body.style.padding = '20px';
const p = document.createElement('p');
p.style.cssText = 'margin-bottom: 15px; color: var(--text-primary);';
p.textContent = message;
body.appendChild(p);
if (details) {
const small = document.createElement('small');
small.style.cssText = 'display: block; margin-top: 10px; color: var(--text-secondary);';
small.textContent = details;
body.appendChild(small);
}
const footer = document.createElement('div');
footer.style.cssText = 'margin-top: 20px; display: flex; justify-content: center;';
const btn = document.createElement('button');
btn.style.cssText = `
background-color: var(--accent-color);
color: white;
padding: 12px 24px;
border-radius: 8px;
border: none;
font-weight: 600;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 12px rgba(233, 69, 96, 0.3);
`;
btn.textContent = btnText || 'Zur Original-Seite';
btn.onclick = () => {
window.open(url || 'https://web.bessa.app/knapp-kantine', '_blank');
modal.classList.add('hidden');
};
footer.appendChild(btn);
body.appendChild(footer);
content.appendChild(body);
modal.appendChild(content);
document.body.appendChild(modal);
document.getElementById('btn-error-redirect').addEventListener('click', () => {
window.location.href = url;
});
requestAnimationFrame(() => {
modal.classList.remove('hidden');
});
}
export function updateAlarmBell() {