Compare commits
2 Commits
1af1668c02
...
2f7381484a
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f7381484a | |||
| 89100e2c9c |
@@ -1,6 +0,0 @@
|
|||||||
# Bessa Credentials (NEVER commit real credentials!)
|
|
||||||
BESSA_EMPLOYEE_NUMBER=YOUR_EMPLOYEE_NUMBER
|
|
||||||
BESSA_PASSWORD=YOUR_PASSWORD
|
|
||||||
|
|
||||||
# Optional: Headless mode (set to false for debugging)
|
|
||||||
PUPPETEER_HEADLESS=true
|
|
||||||
55
RESEARCH.md
55
RESEARCH.md
@@ -1,55 +0,0 @@
|
|||||||
# Bessa API Authentication Research
|
|
||||||
|
|
||||||
This document describes the authentication flow for the Bessa Web App (`web.bessa.app/knapp-kantine`).
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The authentication process follows a multi-step flow involving a guest token and user credentials.
|
|
||||||
|
|
||||||
### 1. Initial Guest Session
|
|
||||||
When the page first loads, it initializes a guest session. This session is associated with a guest token.
|
|
||||||
|
|
||||||
* **Identified Guest Token:** `c3418725e95a9f90e3645cbc846b4d67c7c66131`
|
|
||||||
* **Usage:** Mandatory for the login request itself.
|
|
||||||
|
|
||||||
### 2. User Login
|
|
||||||
The login request is sent to the `/auth/login/` endpoint.
|
|
||||||
|
|
||||||
* **Endpoint:** `POST https://api.bessa.app/v1/auth/login/`
|
|
||||||
* **Headers:**
|
|
||||||
* `Authorization`: `Token <Guest_Token>`
|
|
||||||
* `Content-Type`: `application/json`
|
|
||||||
* `Accept`: `application/json`
|
|
||||||
* `X-Client-Version`: `1.7.0_prod/2026-01-26` (Example)
|
|
||||||
* **Request Body:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"email": "knapp-<EMPLOYEE_NUMBER>@bessa.app",
|
|
||||||
"password": "<PASSWORD>"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
> [!NOTE]
|
|
||||||
> The employee number entered in the UI is automatically transformed into an email format: `knapp-<number>@bessa.app`.
|
|
||||||
|
|
||||||
### 3. Authentication Result
|
|
||||||
A successful login returns a session key.
|
|
||||||
|
|
||||||
* **Response (200 OK):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"key": "dba7d86e83c7f462fd8af96521dea41c4facd8a5"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
* **Usage:** This `key` MUST be used in the `Authorization` header for all subsequent API requests.
|
|
||||||
* **Header Format:** `Authorization: Token dba7d86e83c7f462fd8af96521dea41c4facd8a5`
|
|
||||||
|
|
||||||
### 4. Token Persistence
|
|
||||||
* The token is stored in the browser's `localStorage` under the key `AkitaStores`.
|
|
||||||
* Path: `AkitaStores.auth.token`
|
|
||||||
|
|
||||||
## Implementation Considerations
|
|
||||||
|
|
||||||
For the wrapper implementation:
|
|
||||||
1. **In-Memory Storage**: The token should be handled purely in-memory (e.g., in the user session) to ensure security and follow privacy guidelines.
|
|
||||||
2. **No Persistence**: Credentials or tokens should never be written to disk in a production environment.
|
|
||||||
3. **Automatic Email Transformation**: The login handler should automatically prepend `knapp-` and append `@bessa.app` to the provided employee number to mimic the official app's behavior.
|
|
||||||
@@ -5,7 +5,7 @@ set -e
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
DIST_DIR="$SCRIPT_DIR/dist"
|
DIST_DIR="$SCRIPT_DIR/dist"
|
||||||
CSS_FILE="$SCRIPT_DIR/public/style.css"
|
CSS_FILE="$SCRIPT_DIR/style.css"
|
||||||
JS_FILE="$SCRIPT_DIR/kantine.js"
|
JS_FILE="$SCRIPT_DIR/kantine.js"
|
||||||
|
|
||||||
mkdir -p "$DIST_DIR"
|
mkdir -p "$DIST_DIR"
|
||||||
|
|||||||
2
dist/bookmarklet-payload.js
vendored
2
dist/bookmarklet-payload.js
vendored
File diff suppressed because one or more lines are too long
2
dist/bookmarklet.txt
vendored
2
dist/bookmarklet.txt
vendored
File diff suppressed because one or more lines are too long
2
dist/install.html
vendored
2
dist/install.html
vendored
File diff suppressed because one or more lines are too long
16
dist/kantine-standalone.html
vendored
16
dist/kantine-standalone.html
vendored
@@ -2133,7 +2133,21 @@ body {
|
|||||||
const body = document.createElement('div');
|
const body = document.createElement('div');
|
||||||
body.className = 'card-body';
|
body.className = 'card-body';
|
||||||
|
|
||||||
const sortedItems = [...day.items].sort((a, b) => a.name.localeCompare(b.name));
|
const todayDateStr = new Date().toISOString().split('T')[0];
|
||||||
|
const isToday = day.date === todayDateStr;
|
||||||
|
|
||||||
|
const sortedItems = [...day.items].sort((a, b) => {
|
||||||
|
if (isToday) {
|
||||||
|
const aId = a.articleId || parseInt(a.id.split('_')[1]);
|
||||||
|
const bId = b.articleId || parseInt(b.id.split('_')[1]);
|
||||||
|
const aOrdered = orderMap.has(`${day.date}_${aId}`);
|
||||||
|
const bOrdered = orderMap.has(`${day.date}_${bId}`);
|
||||||
|
|
||||||
|
if (aOrdered && !bOrdered) return -1;
|
||||||
|
if (!aOrdered && bOrdered) return 1;
|
||||||
|
}
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
sortedItems.forEach(item => {
|
sortedItems.forEach(item => {
|
||||||
const itemEl = document.createElement('div');
|
const itemEl = document.createElement('div');
|
||||||
|
|||||||
16
kantine.js
16
kantine.js
@@ -1012,7 +1012,21 @@
|
|||||||
const body = document.createElement('div');
|
const body = document.createElement('div');
|
||||||
body.className = 'card-body';
|
body.className = 'card-body';
|
||||||
|
|
||||||
const sortedItems = [...day.items].sort((a, b) => a.name.localeCompare(b.name));
|
const todayDateStr = new Date().toISOString().split('T')[0];
|
||||||
|
const isToday = day.date === todayDateStr;
|
||||||
|
|
||||||
|
const sortedItems = [...day.items].sort((a, b) => {
|
||||||
|
if (isToday) {
|
||||||
|
const aId = a.articleId || parseInt(a.id.split('_')[1]);
|
||||||
|
const bId = b.articleId || parseInt(b.id.split('_')[1]);
|
||||||
|
const aOrdered = orderMap.has(`${day.date}_${aId}`);
|
||||||
|
const bOrdered = orderMap.has(`${day.date}_${bId}`);
|
||||||
|
|
||||||
|
if (aOrdered && !bOrdered) return -1;
|
||||||
|
if (!aOrdered && bOrdered) return 1;
|
||||||
|
}
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
sortedItems.forEach(item => {
|
sortedItems.forEach(item => {
|
||||||
const itemEl = document.createElement('div');
|
const itemEl = document.createElement('div');
|
||||||
|
|||||||
1148
public/app.js
1148
public/app.js
File diff suppressed because it is too large
Load Diff
@@ -1,125 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Kantine Weekly Menu</title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<header class="app-header">
|
|
||||||
<div class="header-content">
|
|
||||||
<div class="brand">
|
|
||||||
<span class="material-icons-round logo-icon">restaurant_menu</span>
|
|
||||||
<div class="brand-text">
|
|
||||||
<h1>Kantinen Übersicht</h1>
|
|
||||||
<div id="last-updated-subtitle" class="subtitle"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="header-week-info" class="header-week-info">
|
|
||||||
<!-- Populated by JavaScript -->
|
|
||||||
</div>
|
|
||||||
<div id="weekly-cost-display" class="weekly-cost hidden">
|
|
||||||
<!-- Populated by JS -->
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<button id="btn-refresh" class="icon-btn" aria-label="Menüdaten aktualisieren"
|
|
||||||
title="Menüdaten aktualisieren">
|
|
||||||
<span class="material-icons-round">refresh</span>
|
|
||||||
</button>
|
|
||||||
<div class="nav-group">
|
|
||||||
<button id="btn-this-week" class="nav-btn active">Diese Woche</button>
|
|
||||||
<button id="btn-next-week" class="nav-btn">Nächste Woche</button>
|
|
||||||
</div>
|
|
||||||
<button id="theme-toggle" class="icon-btn" aria-label="Toggle Theme">
|
|
||||||
<span class="material-icons-round theme-icon">light_mode</span>
|
|
||||||
</button>
|
|
||||||
<button id="btn-login-open" class="user-badge-btn icon-btn-small">
|
|
||||||
<span class="material-icons-round">login</span>
|
|
||||||
<span>Anmelden</span>
|
|
||||||
</button>
|
|
||||||
<div id="user-info" class="user-badge hidden">
|
|
||||||
<span class="material-icons-round">person</span>
|
|
||||||
<span id="user-id-display"></span>
|
|
||||||
<button id="btn-logout" class="icon-btn-small" aria-label="Logout">
|
|
||||||
<span class="material-icons-round">logout</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div id="login-modal" class="modal hidden">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2>Login</h2>
|
|
||||||
<button id="btn-login-close" class="icon-btn" aria-label="Close">
|
|
||||||
<span class="material-icons-round">close</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<form id="login-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="employee-id">Mitarbeiternummer</label>
|
|
||||||
<input type="text" id="employee-id" name="employee-id" placeholder="z.B. 2041" required>
|
|
||||||
<small class="help-text">Deine offizielle Knapp Mitarbeiternummer.</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Passwort</label>
|
|
||||||
<input type="password" id="password" name="password" placeholder="Bessa Passwort" required>
|
|
||||||
<small class="help-text">Das Passwort für deinen Bessa Account.</small>
|
|
||||||
</div>
|
|
||||||
<div id="login-error" class="error-msg hidden"></div>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button type="submit" class="btn-primary wide">Einloggen</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="progress-modal" class="modal hidden">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2>Menüdaten aktualisieren</h2>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" style="padding: 20px;">
|
|
||||||
<div class="progress-container">
|
|
||||||
<div class="progress-bar">
|
|
||||||
<div id="progress-fill" class="progress-fill"></div>
|
|
||||||
</div>
|
|
||||||
<div id="progress-percent" class="progress-percent">0%</div>
|
|
||||||
</div>
|
|
||||||
<p id="progress-message" class="progress-message">Initialisierung...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main class="container">
|
|
||||||
<div id="last-updated-banner" class="banner hidden">
|
|
||||||
<span class="material-icons-round">update</span>
|
|
||||||
<span id="last-updated-text">Gerade aktualisiert</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="loading" class="loading-state">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<p>Lade Menüdaten...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="menu-container" class="menu-grid">
|
|
||||||
<!-- Dynamic Content -->
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer class="app-footer">
|
|
||||||
<p>Bessa Knapp-Kantine Wrapper • <span id="current-year">2026</span></p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script src="app.js"></script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user