Alpha release

This commit is contained in:
Tim Berchtold 2025-07-04 14:38:36 +02:00
parent 62681acd16
commit eeda5062ae
14 changed files with 842 additions and 1 deletions

77
Server/DB/handler.py Normal file
View file

@ -0,0 +1,77 @@
from __future__ import annotations
import json
import datetime
import os
from typing import List
from sqlalchemy import create_engine, BLOB, Column, Boolean, Float, Integer, String, JSON, DateTime, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import DeclarativeBase, sessionmaker, relationship, Mapped, mapped_column
from loguru import logger
from flask_login import UserMixin
# create an in-memory SQLite database
engine = create_engine('sqlite:///db.sqlite', echo=True)
Session = sessionmaker(bind=engine)
session = Session()
Base = declarative_base()
class User(UserMixin, Base):
__tablename__ = 'User'
username = Column(String, primary_key=True, unique=True)
hashed_password = Column(String)
salt = Column(String)
class Log(Base): #TODO uSe
__tablename__ = 'Log'
number = Column(Integer, primary_key=True)
created_at = Column(DateTime, default=datetime.datetime.now)
desk_ = Column(Integer)
ip = Column(Integer)
def __init__(self, desk, ip, tel_nr):
self.desk = desk
self.ip = ip
class QR(Base): #TODO: Decide if encryption Salting hasing and anti rainbow attack is eeded for the qr codes?
__tablename__ = 'qr'
desk = Column(Integer, primary_key=True)
qr_secret = Column(Integer)
qr_img = Column(BLOB)
def __init__(self, desk, qr_img, qr_secret=0):
self.desk = desk
self.qr_secret = qr_secret
self.qr_img = qr_img
class Product(Base):
__tablename__ = 'product'
id = mapped_column(Integer, primary_key=True)
name = Column(String)
quantity = Column(Integer)
order_id = mapped_column(ForeignKey("order.id"))
order = relationship("Order", back_populates="order")
def __init__(self, quantity,name, order_id):
self.quantity = quantity
self.name = name
self.order_id = order_id
self.date = datetime.datetime.now()
class Order(Base):
__tablename__ = 'order'
id = mapped_column(Integer, primary_key=True)
order = relationship("Product", back_populates="order")
desk_number = Column(Integer)
finished = Column(Boolean)
def __init__(self, desk_number, finished=False, ):
self.desk_number = desk_number
self.desk = 1
#TODO: replace product-order.json whit json object POST from flask
# That means a UI for setting all values.
self.finished = finished
Base.metadata.create_all(engine)

147
Server/DB/querys.py Normal file
View file

@ -0,0 +1,147 @@
from loguru import logger
import json
import bcrypt
from Server.DB.handler import QR, Product, Order, session, User
class compare:
def is_user_pass_valid(username, password):
session_username = session.query(User).filter(User.username == username).one()
session.commit()
print(session_username.hashed_password)
if bcrypt.hashpw(password.encode('utf-8'), session_username.salt) == session_username.hashed_password.encode('utf-8'):
return True
return False
def is_QRSecret_valid(desk, secret):#TODO FIX! #Feature = True #TODO:Encryption Salting hasing and anti rainbow attack
q = session.query(QR).\
filter(QR.desk.like(int(desk))).\
order_by(QR.qr_secret)
q_QR = q.with_session(session).one()
if q_QR.qr_secret == secret:
return True
else:
return False
def process_main(ordered_list, desk):
'''
Get all verified Products from the order to the db entry
Input: Array ordered_list, INT desk
OUT: True, False if succeded adding to db
'''
if compare.verifie_order(ordered_list):
#products = Product()
order = Order(desk)
session.add(order)
session.commit()
for product in ordered_list:
if int(product[0]) > 0: #dont add not ordered to list
productDB = Product(quantity=int(product[0]),name=product[1],order_id = order.id)
session.add(productDB)
session.commit()
logger.success("Order at desk "+ str(desk) +" resived")
# add a new ordered_list to the database
return True
else:
print("There was an invalid order! Someone messing whit source?")
return False
#TODO: Placeholder
def verifie_order(ordered_list):
'''
if self.desk > self.desk_numbers:
return False
'''
valid_products_list = get.valid_products()
for product in ordered_list:
print(product[1],' ', valid_products_list)
if product[1] in valid_products_list: #product[0] is quantiiy 1 is name_product
pass
else:
return False
return True
class add:
def _create_user(username, password):
if session.query(User).filter_by(username = username).count() < 1:
bytes = password.encode('utf-8')
salt = bcrypt.gensalt()
hash = bcrypt.hashpw(bytes, salt).decode('utf-8')
user = User(username = username, hashed_password = hash, salt= salt)
session.add(user)
session.commit()
def _order(orders, desk):
'''
add order to db
INPUT: DICT orders, desk
OUT: True succes False failure
'''
pass
class get:
'''
def user_in_User(username):
calls db if user in User table
q_session = session.query(User).filter(User.username == username).one()
session.commit()
if q_session:
return q_session.id
return False
'''
def valid_products(get_json_=False):
with open(config['SETTINGS']['path_json_settings'], 'r') as file:
#Parse Json Product List
products = json.load(file)
if get_json_:
return products
else:
valide_products = []
for category in products.get('products'):
for product in products.get('products').get(category):
print(product)
if int(product['quantity']) >= 0:
valide_products.append(product['name'])
return valide_products
def all_orders():
# All unfinished orders (finished is False or string "False")
result = []
desk = 1
orders = session.query(Order).filter(Order.finished == 0).all()
session.commit()
for order in orders:
products = session.query(Product).filter(Product.order_id == order.id , Order.desk_number == desk).all()
session.commit()
list_products = []
for p in products:
#What it shows in orders.html
list_products.append(" x" + str(p.quantity) +" "+ p.name )
result.append((order.desk_number, list_products, order.id))
return result
class update:
#TODO GET JSON to -> DB
def update_products_fromJSON(self):
pass
class set:
def finish_order(order_number):
order = session.query(Order).filter(Order.id == int(order_number)).first()
if order:
order.finished = 1 # or "True
session.commit()
logger.info("Order "+str(order_number)+" marked as finished.")
return True
else:
return False

151
Server/Host/flaskApp.py Normal file
View file

@ -0,0 +1,151 @@
from Server.DB.handler import Order, User
from flask import Flask, abort,flash, render_template, request, redirect, url_for, jsonify
# create a session to manage the connection to the database
import time
import configparser
from Server.DB.handler import session, QR
from Server.DB.querys import compare, get, set, add
from flask_login import LoginManager, login_required, login_user, logout_user
from flask_wtf import FlaskForm
from flask_wtf.csrf import CSRFProtect
from wtforms import StringField, PasswordField, SubmitField, BooleanField
from Tools.qrGenerator import generate_QR_Code
import os
config = configparser.ConfigParser()
config.sections()
config.read('Options.ini')
login_manager = LoginManager()
csrf = CSRFProtect()
app = Flask(__name__)
SECRET_KEY = os.urandom(32)
app.config['SECRET_KEY'] = SECRET_KEY
csrf.init_app(app)
login_manager.init_app(app)
#default db values
# create the Base if not present whit default values like admin+password and qr codes.
if config['OTHER']['first_startup'] == 'True':
print("Please enter a Secure Admin Password:")
add._create_user('Admin',input())
config.set('OTHER', 'first_startup', 'False')
with open('Options.ini', 'w') as configfile:
config.write(configfile)
generate_QR_Code()
@login_manager.user_loader
def user_loader(username):
user = User()
user.id = username
print(username, user.id)
return user
class LoginForm(FlaskForm):
username = StringField('name')
password = PasswordField('Password')
remember_me = BooleanField('Remember Me') #TODO
submit = SubmitField('Submit')
@app.route("/",)
def view_form():
return "loaded"
@app.route("/login", methods=['GET','POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = user_loader(form.username.data)
#TODO if username not in User:
#TODO return if username or pass wrong a red textbox in ui...
if user and compare.is_user_pass_valid(form.username.data, form.password.data):
login_user(user) # store user id in session
#TODO url_has_allowed_host_and_scheme should check if the url is safe
# for redirects, meaning it matches the request host.
# See Django's url_has_allowed_host_and_scheme for an example.
# if not url_has_allowed_host_and_scheme(next, request.host):
# return abort(400)
#next = request.args.get('next') is written in documentaion FLASK
#return redirect(next, url_for('orders')) # redirect to orders page
return redirect(url_for('orders')) # redirect to orders page
return render_template('login.html', form=form)
@app.route("/logout") #TODO make a button
@login_required
def logout():
logout_user()
return redirect('/')
@app.route("/orders", methods=['GET','POST'])
@login_required
def orders(): #TODO on new orderGet refresh orders list
if request.method == 'POST':
order_id = request.form.get('order_id')
action = request.form.get('action') # 'finish' or 'undo'
order = session.get(Order, int(order_id))
if order: #SQL error handeling just in case
if action == 'finish':
order.finished = True
elif action == 'undo':
order.finished = False
session.commit()
return jsonify(success=True)
return jsonify(success=False)
return render_template('orders.html', orders=get.all_orders())
@app.route("/order_get", methods=['GET','POST'])
def order_get():
desk= 1
#POST order
if request.method == 'POST' and config['SETTINGS']['lockqrcode_whit_secret'] == True:
ordered_list = []
desk = request.form['desk']
form = request.form
for key in form:
if key.startswith('order-name.'):
name = key.partition('.')[-1]
value = request.form[key]
ordered_list.append([value,name])
# Adding order to DB
if compare.process_main(ordered_list, desk):
return '<h1>your order got ressived!</h1>'
return '<h1>your order coudnt be ressived try again Error:SQL </h1>'
# Veriefie if auth or not from GET
elif request.method == 'GET':
desk = int(request.args['desk'])
secret = int(request.args['secret'])
try:#TODO FIX! #Feature = True #TODO:Encryption Salting hasing and anti rainbow attack for qr code?? (needed?)
if config['SETTINGS']['lockqrcode_whit_secret'] and compare.is_QRSecret_valid(desk,secret):
return render_template('index.html', desk=desk, MAX_DESKS=config['DEFAULT']['max_desks'], orderableItems = get.valid_products(get_json_=True))
elif config['SETTINGS']['lockqrcode_whit_secret'] == False:
return render_template('index.html', desk=desk, MAX_DESKS=config['DEFAULT']['max_desks'],orderableItems = get.valid_products(get_json_=True))
return '<h1>404 wrong Secret?</h1>' #TODO Make the HTML prettier for all
except:
return '<h1>Server/code issue?</h1>'
elif config['SETTINGS']['lockqrcode_whit_secret'] == True:
return '<h1>your LOCKQRCODE is invalid.</h1>'
else:
return render_template('index.html',desk=desk, MAX_DESKS=config['DEFAULT']['max_desks'],orderableItems = get.valid_products(get_json_=True))
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

View file

@ -0,0 +1,7 @@
.container {
justify-content: space-evenly;
}
.container div {
background-color: aquamarine;
}

View file

@ -0,0 +1,79 @@
html body {
min-height: 100%;
list-style-type: none;
}
body {
height: 98vh;
background: white;
}
/* FLEXBOX DEFINITION */
.flex-container {
height: 100%;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
}
.box {
margin: 5px;
line-height: 2vh;
color: white;
font-weight: bold;
font-size: 3em;
text-align: center;
align-items: center;
}
/* BOX 1 DESK NR */
.desk-nr {
margin-top: 5vw;
background: lightcyan;
color: black;
font-size: 7vw;
}
.desk-nr div {
padding-top: 5vw;
margin-bottom: 6vw;
}
.desk-select {
padding-top: 1vw;
padding-bottom: 1vw;
}
/* BOX 3 CONTENT ORDER */
.content {
margin-top: 5vw;
flex-grow: 1;
background: lightcyan;
border: rgb(46, 42, 42);
color: black;
font-size: 8vw;
}
.content div {
margin-top:8vw;
}
.products {
font-size: 4vw;
}
.content div summary {
color: rgb(46, 42, 42);
line-height: 8vh;
border-bottom: 2px double rgb(46, 42, 42) ;
margin-bottom: 5vw;
}
/* BOX 4 CONTENT ORDER BUTTON SEND */
.order {
background-color: rgb(209, 45, 45);
padding: 1vw;
line-height: 8vh;
}

View file

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/style.css">
<title>Document</title>
</head>
<body>
<form name="order" onsubmit="return validateForm()" action="{{ url_for('order_get') }}" method="POST" class="flex-container">
<li class="box desk-nr">Tisch Nummer
<div>
<select name="desk" class="desk-select">
{% for e in range(1, MAX_DESKS+1) %}
{% if desk == e %}
<option selected>{{e}}</option>
{% else %}
<option>{{e}}</option>
{% endif %}
{% endfor %}
</select>
</div>
</li>
<li class="box content">Menu
{% for category in orderableItems.get('products') %}
<div>
<details>
<summary>{{ category }}</summary>
{% for product in orderableItems.get('products').get(category)%}
<div class="products" id="{ product['name'] }">
{{product['name']}}
<input id="points" type="number" value=0 name="{{ "order-name." + product['name'] }}" step="1">
</div>
{% endfor %}
</details>
</div>
{% endfor %}
</li>
<button type="submit" class="box order">Weiter</button>
</form>
</body>
</html>

View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>Login Page</title>
</head>
<body>
<h1>Login Page</h1>
<form method="POST" action="{{ url_for('login') }}">
{{ form.csrf_token }}
{{ form.username.label }}
{{ form.username }}
<br>
<br>
{{ form.password.label }}
{{ form.password }}
<br>
<p>{{ form.remember_me }} {{ form.remember_me.label }}</p>
<br>
{{ form.submit }}
</form>
</body>
</html>

View file

@ -0,0 +1,125 @@
<!doctype html>
<html>
<head>
<title>Orders</title>
<style>
table { border-collapse: collapse; width: 80%; margin: 40px auto; }
th, td { border: 1px solid #cccccc; padding: 12px 18px; }
th { background: #f0f0f0; }
.finish-btn {
background-color: #4CAF50; /* Green */
color: white;
border: none;
padding: 7px 18px;
cursor: pointer;
border-radius: 5px;
}
.undo-btn {
background-color: #f44336; /* Red */
color: white;
border: none;
padding: 7px 18px;
cursor: pointer;
border-radius: 5px;
}
.finish-btn:hover { background-color: #388e3c; }
.undo-btn:hover { background-color: #d32f2f; }
</style>
</head>
<body>
<h1 style="text-align:center;">Current Orders</h1>
<table>
<tr>
<th>Desk</th>
<th>Products</th>
<th>Action</th>
<th>Order-Nr</th>
</tr>
{% for order in orders%}
<tr id="order-{{ order[2] }}">
<td>{{ order[0] }}</td>
<td>
<ul>
{% for product in order[1] %}
<li>{{ product }}</li>
{% endfor %}
</ul>
</td>
<td>
<button
class="finish-btn"
onclick="markFinished({{ order[2] }})"
id="btn-{{ order[2] }}">
Finished
</button>
</td>
<td>order {{ order[2] }} </td>
</tr>
{% else %}
<tr><td colspan="3">No open orders.</td></tr>
{% endfor %}
</table>
<script>
// Track active timers for each order
window.activeTimers = {};
function markFinished(orderId) {
fetch('/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `order_id=${orderId}&action=finish`
})
.then(response => response.json())
.then(data => {
if (data.success) {
const btn = document.getElementById(`btn-${orderId}`);
btn.className = 'undo-btn';
btn.onclick = () => undoFinished(orderId);
// Start countdown (10s)
let secondsLeft = 10;
btn.textContent = `Undo (${secondsLeft}s)`;
// Store timer reference
window.activeTimers[orderId] = setInterval(() => {
secondsLeft--;
btn.textContent = `Undo (${secondsLeft}s)`;
// Auto-remove after 10s
if (secondsLeft <= 0) {
clearInterval(window.activeTimers[orderId]);
document.getElementById(`order-${orderId}`).remove();
delete window.activeTimers[orderId];
}
}, 1000);
}
});
}
function undoFinished(orderId) {
// Clear the countdown timer
if (window.activeTimers[orderId]) {
clearInterval(window.activeTimers[orderId]);
delete window.activeTimers[orderId];
}
fetch('/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `order_id=${orderId}&action=undo`
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Revert button to original state
const btn = document.getElementById(`btn-${orderId}`);
btn.className = 'finish-btn';
btn.textContent = 'Finished';
btn.onclick = () => markFinished(orderId);
}
});
}
</script>
</body>
</html>

View file

@ -0,0 +1,37 @@
{
"products": {
"Süsses": [
{
"name": "Haribos",
"price": 4,
"quantity": 2
},
{
"name": "Skittles",
"price": 4,
"quantity": 2
}
],
"Getränke": [
{
"name": "IceTea",
"price": 4,
"quantity": 2
},
{
"name": "Fanta",
"price": 4,
"quantity": 5
}
],
"Öm": [
{
"name": "Molecule Man",
"price": 10,
"quantity": 5
}
]
}
}