Compare commits

..

No commits in common. "0358ef44e4f2c1b8852a0f570899365934ed2a1d" and "7898345a06c23dee96226a25a9a529832a57ef59" have entirely different histories.

12 changed files with 196 additions and 280 deletions

2
.gitignore vendored
View File

@ -5,8 +5,6 @@ Testplan.md
.venv .venv
users/ users/
backup/ backup/
settings/
Archiv/ Archiv/
Docker/ Docker/
docker-work/ docker-work/
Wiki/

View File

@ -1,13 +1,14 @@
FROM python:latest FROM debian:latest
RUN apt update && apt upgrade -y RUN apt update && apt upgrade -y
RUN apt install locales -y RUN apt install python3 python3-pip python3.11-venv locales -y
RUN mkdir /app RUN mkdir /app
RUN mkdir /.venv RUN mkdir /.venv
RUN mkdir /backup RUN mkdir /backup
RUN mkdir /settings RUN mkdir /settings
RUN pip install nicegui RUN python3 -m venv /.venv
RUN pip install segno RUN /.venv/bin/pip install nicegui
RUN pip install python-dateutil RUN /.venv/bin/pip install segno
RUN /.venv/bin/pip install python-dateutil
RUN sed -i '/de_DE.UTF-8/s/^# //g' /etc/locale.gen && \ RUN sed -i '/de_DE.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen locale-gen
@ -18,4 +19,4 @@ ENV LC_ALL de_DE.UTF-8
COPY main.py /app/main.py COPY main.py /app/main.py
COPY lib /app/lib/ COPY lib /app/lib/
EXPOSE 8090 EXPOSE 8090
ENTRYPOINT ["python", "/app/main.py"] ENTRYPOINT ["/.venv/bin/python", "/app/main.py"]

View File

@ -5,9 +5,9 @@ import os
server = 'gitea.am-td.de' server = 'gitea.am-td.de'
server_user = 'alexander' server_user = 'alexander'
#if os.getuid() == 0: if os.getuid() == 0:
subprocess.run(["docker", "build", "--force-rm", "-t", f"{server}/{server_user}/{app_title.lower()}:{app_version}", "."]) subprocess.run(["docker", "build", "--force-rm", "-t", f"{server}/{server_user}/{app_title.lower()}:{app_version}", "."])
if input("docker-compose erstellen j=JA ") == "j": if input("docker-compose erstellen j=JA ") == "j":
userfolder = input("Pfad für Benutzerdaten /users:") userfolder = input("Pfad für Benutzerdaten /users:")
backupfolder = input("Pfad für Backupdaten /backup:") backupfolder = input("Pfad für Backupdaten /backup:")
settingsfolder = input("Pfad für Einstellungen /settings:") settingsfolder = input("Pfad für Einstellungen /settings:")
@ -27,5 +27,5 @@ services:
with open('docker-compose.yml', 'w') as docker_compose: with open('docker-compose.yml', 'w') as docker_compose:
docker_compose.write(docker_compose_content) docker_compose.write(docker_compose_content)
#else: else:
# print("Es werden Root-Rechte benötigt.") print("Es werden Root-Rechte benötigt.")

View File

@ -1,14 +1,13 @@
services: services:
zeiterfassung: zeiterfassung:
image: gitea.am-td.de/alexander/zeiterfassung:beta-2025.0.2 image: gitea.am-td.de/alexander/zeiterfassung:beta-2025.0.1
restart: always restart: always
ports: ports:
- 8090:8090 - 8090:8090
environment: environment:
- PYTHONUNBUFFERED=1 - PYTHONUNBUFFERED=1
volumes: volumes:
- ./users:/users - ./docker-work/users:/users
- ./backup:/backup - ./docker-work/backup:/backup
- ./settings:/settings - ./docker-work/settings:/settings
- /etc/localtime:/etc/localtime:ro

View File

@ -4,7 +4,6 @@ import dateutil.easter
from dateutil.easter import * from dateutil.easter import *
from nicegui import ui, app, events from nicegui import ui, app, events
from nicegui.functions.navigate import navigate
from nicegui.html import button from nicegui.html import button
from nicegui.events import KeyEventArguments from nicegui.events import KeyEventArguments
@ -45,10 +44,6 @@ def page_admin():
updates_available = ValueBinder() updates_available = ValueBinder()
updates_available.value = False updates_available.value = False
delete_binder = ValueBinder()
delete_binder.value = True
delete_info = ValueBinder()
delete_info.value = False
enabled_because_not_docker = ValueBinder enabled_because_not_docker = ValueBinder
if is_docker(): if is_docker():
@ -70,13 +65,6 @@ def page_admin():
def update_userlist(): def update_userlist():
nonlocal userlist nonlocal userlist
userlist = list_users() userlist = list_users()
# Benutzerliste um No Time Users bereinigen
users_to_remove = [ ]
for entry in userlist:
if entry in get_no_time_users_list():
users_to_remove.append(entry)
for i in users_to_remove:
userlist.remove(i)
update_userlist() update_userlist()
@ -771,27 +759,17 @@ def page_admin():
with ui.tab_panel(settings): with ui.tab_panel(settings):
with ui.grid(columns='auto auto'): with ui.grid(columns='auto auto'):
with ui.card(): with ui.card():
ui.label("Benutzereinstellungen:").classes('text-bold') ui.label("Administrationsbenutzer:").classes('text-bold')
with ui.grid(columns=2).classes('items-center'): with ui.grid(columns=2).classes('items-baseline'):
ui.label("Benutzername des Adminstrators:")
admin_user = ui.input().tooltip("Geben Sie hier den Benutzernamen für den Adminstationsnutzer ein")
admin_user.value = data["admin_user"]
ui.label("Passwort des Administrators:")
admin_password = ui.input(password=True).tooltip("Geben Sie hier das Passwort für den Administationsnutzer ein. Merken Sie sich dieses Passwort gut. Es kann nicht über das Webinterface zurückgesetzt werden.")
ui.label("Benutzer mit Administrationsrechten:")
user_switch_list = []
with ui.grid(columns=2).classes('gap-y-0'):
for i in list_users():
user_switch_list.append(ui.switch(i))
for i in user_switch_list:
if i.text in get_admin_list():
i.value = True
secret = data["secret"] secret = data["secret"]
ui.separator().classes('col-span-2')
ui.label("Benutzer ohne Zeiterfassung:")
no_time_user_switch_list = [ ]
with ui.grid(columns=2).classes('gap-y-0'):
for i in list_users():
no_time_user_switch_list.append(ui.switch(i))
for item in no_time_user_switch_list:
if item.text in get_no_time_users_list():
item.value = True
with ui.card(): with ui.card():
ui.label("Systemeinstellungen:").classes('text-bold') ui.label("Systemeinstellungen:").classes('text-bold')
with ui.grid(columns=2).classes('items-baseline'): with ui.grid(columns=2).classes('items-baseline'):
@ -1067,30 +1045,11 @@ def page_admin():
holiday_section() holiday_section()
def save_admin_settings(): def save_admin_settings():
admin_users = { } write_adminsetting("admin_user", admin_user.value)
admin_counter = -1 if admin_password.value != "":
for i in user_switch_list: write_adminsetting("admin_password", hash_password(admin_password.value))
if i.value == True:
admin_counter += 1
admin_users[str(admin_counter)] = i.text
if len(admin_users) == 0:
with ui.dialog() as dialog, ui.card():
ui.label("Es wurde kein Administrationsbenutzer ausgewählt. Mindestens ein Benutzer muss Administrationsrechte haben.")
ui.button("OK", on_click=dialog.close)
dialog.open()
else: else:
write_adminsetting("admin_password", data["admin_password"])
no_time_users = { }
no_time_users_counter = -1
for i in no_time_user_switch_list:
if i.value == True:
no_time_users_counter += 1
no_time_users[str(no_time_users_counter)] = i.text
old_no_time_users_list = get_no_time_users_list()
write_adminsetting("admin_user", admin_users)
write_adminsetting("no_time_users", no_time_users)
write_adminsetting("port", port.value) write_adminsetting("port", port.value)
write_adminsetting("secret", secret) write_adminsetting("secret", secret)
write_adminsetting("touchscreen", touchscreen_switch.value) write_adminsetting("touchscreen", touchscreen_switch.value)
@ -1112,12 +1071,6 @@ def page_admin():
reset_visibility.value = False reset_visibility.value = False
timetable.refresh() timetable.refresh()
if not set(old_no_time_users_list) == set(get_no_time_users_list()):
with ui.dialog() as infobox, ui.card():
ui.label("Benutzer ohne Zeiterfassung wurden geändert. Die Seite wird neu geladen.")
ui.button("OK", on_click=ui.navigate.reload)
infobox.open()
with ui.button("Speichern", on_click=save_admin_settings): with ui.button("Speichern", on_click=save_admin_settings):
with ui.tooltip(): with ui.tooltip():
ui.label("Hiermit werden sämtliche oben gemachten Einstellungen gespeichert.\nGgf. müssen Sie die Seite neu laden um die Auswirkungen sichtbar zu machen.").style('white-space: pre-wrap') ui.label("Hiermit werden sämtliche oben gemachten Einstellungen gespeichert.\nGgf. müssen Sie die Seite neu laden um die Auswirkungen sichtbar zu machen.").style('white-space: pre-wrap')
@ -1127,6 +1080,7 @@ def page_admin():
workhours = [ ] workhours = [ ]
with ui.row(): with ui.row():
def user_selection_changed(): def user_selection_changed():
try: try:
if user_selection.value != None: if user_selection.value != None:
@ -1135,12 +1089,7 @@ def page_admin():
fullname_input.value = current_user.fullname fullname_input.value = current_user.fullname
#password_input.value = current_user.password #password_input.value = current_user.password
usersettingscard.visible = True usersettingscard.visible = True
if current_user.username in get_admin_list():
delete_info.value = True
delete_binder.value = False
else:
delete_info.value = False
delete_binder.value = True
api_key_input.value = current_user.api_key api_key_input.value = current_user.api_key
api_link_column.clear() api_link_column.clear()
@ -1213,7 +1162,7 @@ def page_admin():
ui.label("Benutzername wurde geändert.").classes('text-bold') ui.label("Benutzername wurde geändert.").classes('text-bold')
ui.label(f"Benutzerdaten werden in den neuen Ordner {username_input.value} verschoben.") ui.label(f"Benutzerdaten werden in den neuen Ordner {username_input.value} verschoben.")
ui.label("Sollen die Einstellungen gespeichert werden?") ui.label("Sollen die Einstellungen gespeichert werden?")
with ui.row().classes('w-full justify-center'): with ui.row():
ui.button("Speichern", on_click=save_settings) ui.button("Speichern", on_click=save_settings)
ui.button("Abbrechen", on_click=dialog.close) ui.button("Abbrechen", on_click=dialog.close)
dialog.open() dialog.open()
@ -1266,9 +1215,9 @@ def page_admin():
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
ui.label("Sollen die Änderungen an den Arbeitsstunden und/oder Urlaubstagen gespeichert werden?") ui.label("Sollen die Änderungen an den Arbeitsstunden und/oder Urlaubstagen gespeichert werden?")
with ui.row().classes('justify-center w-full'): with ui.row():
ui.button("Speichern", on_click=save_settings) ui.button("Speichern", on_click=save_settings)
ui.button("Abbrechen", on_click=dialog.close) ui.button("Abrrechen", on_click=dialog.close)
dialog.open() dialog.open()
def delete_workhour_entry(): def delete_workhour_entry():
@ -1372,18 +1321,16 @@ def page_admin():
ui.button("Neu", on_click=new_api_key).tooltip("Neuen API-Schlüssel erzeugen. Wird erst beim Klick auf Speichern übernommen und entsprechende Links und QR-Codes aktualisiert") ui.button("Neu", on_click=new_api_key).tooltip("Neuen API-Schlüssel erzeugen. Wird erst beim Klick auf Speichern übernommen und entsprechende Links und QR-Codes aktualisiert")
ui.label('Aufruf zum Stempeln:') ui.label('Aufruf zum Stempeln:')
global api_link_column global api_link_column
with ui.expansion("").classes('w-full'):
with ui.column().classes('gap-0') as api_link_column: with ui.column().classes('gap-0') as api_link_column:
global stamp_link global stamp_link
stamp_link = [ ] stamp_link = [ ]
for i in app.urls: for i in app.urls:
stamp_link.append(ui.link(f'{i}/api/stamp/"API-Schüssel"')) stamp_link.append(ui.link(f'{i}/api/stamp/"API-Schüssel"'))
ui.label("Administratoren können nicht gelöscht werden. Um das Konto zu löschen, müssen Sie ihm zuerst die Administrationsrechte entziehen.").bind_visibility_from(delete_info, 'value').classes('font-bold text-red')
with ui.grid(columns=2): with ui.grid(columns=2):
ui.button("Speichern", on_click=save_user_settings).tooltip("Klicken Sie hier um die Änderungen zu speichern.") ui.button("Speichern", on_click=save_user_settings).tooltip("Klicken Sie hier um die Änderungen zu speichern.")
ui.button("Löschen", on_click=del_user).bind_enabled_from(delete_binder, 'value') ui.button("Löschen", on_click=del_user)
usersettings_card() usersettings_card()

View File

@ -6,7 +6,7 @@ from pathlib import Path
import hashlib import hashlib
app_title = "Zeiterfassung" app_title = "Zeiterfassung"
app_version = "beta-2025.0.2" app_version = "beta-2025.0.1"
# Standardpfade # Standardpfade
@ -36,9 +36,8 @@ status_out = "ausgestempelt"
# Standardadmin Settings: # Standardadmin Settings:
standard_adminsettings = { "admin_user": { standard_adminsettings = { "admin_user": "admin",
0: "admin"}, "admin_password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
"no_time_users": { },
"port": "8090", "port": "8090",
"secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise", "secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise",
"times_on_touchscreen": True, "times_on_touchscreen": True,
@ -56,9 +55,9 @@ standard_adminsettings = { "admin_user": {
# Standard User Settings: # Standard User Settings:
standard_usersettings = { standard_usersettings = {
"username": "admin", "username": "default",
"fullname": "Administrator", "fullname": "Standardbenutzer",
"password": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918", "password": "37a8eec1ce19687d132fe29051dca629d164e2c4958ba141d5f4133a33f0688f",
"api_key": "1234567890", "api_key": "1234567890",
"workhours": { } "workhours": { }
} }

41
lib/login.py Normal file
View File

@ -0,0 +1,41 @@
from datetime import datetime
from nicegui import ui, app
from lib.web_ui import *
from lib.users import *
from lib.definitions import *
from calendar import monthrange
import hashlib
import calendar
import locale
@ui.page('/login')
def page_login():
# Settingsdatei einlesen
data = load_adminsettings()
def login():
nonlocal data
if username.value == data["admin_user"]:
print(f"Input Hash: {hash_password(password.value)} gespeichert: {data['admin_password']}")
if hash_password(password.value) == data["admin_password"]:
app.storage.user['authenticated'] = True
ui.navigate.to("/admin")
else:
ui.notify("Login fehlgeschlagen")
#ui.markdown(f"## {app_title} {app_version}")
#ui.markdown("Bitte einloggen")
pageheader("Bitte einloggen:")
with ui.grid(columns=2):
ui.markdown("Benutzer:")
username = ui.input('Benutzername')
ui.markdown("Passwort:")
password = ui.input('Passwort', password=True)
ui.button(text="Login", on_click=lambda: login())

View File

@ -41,15 +41,15 @@ def page_touchscreen():
def set_columns(width): def set_columns(width):
nonlocal number_of_columns nonlocal number_of_columns
if width > 1400: if width > 1400:
number_of_columns = 5 number_of_columns = 6
elif width > 1200: elif width > 1200:
number_of_columns = 4 number_of_columns = 5
elif width > 900: elif width > 900:
number_of_columns = 3 number_of_columns = 4
elif width > 750: elif width > 750:
number_of_columns = 2 number_of_columns = 3
else: else:
number_of_columns = 1 number_of_columns = 2
user_buttons.refresh() user_buttons.refresh()
ui.on('resize', lambda e: set_columns(e.args['width'])) ui.on('resize', lambda e: set_columns(e.args['width']))
@ -71,12 +71,11 @@ def page_touchscreen():
</script> </script>
''') ''')
with ui.grid(columns=number_of_columns).classes('w-full'): with ui.grid(columns=number_of_columns).classes('w-full center'):
for name in userlist: for name in userlist:
current_user = user(name) current_user = user(name)
if not current_user.username in get_no_time_users_list(): current_button = ui.button(on_click=lambda name=name: button_click(name)).classes(f'w-md h-full min-h-[{admin_settings["button_height"]}px]')
current_button = ui.button(on_click=lambda name=name: button_click(name)).classes(f'h-full min-h-[{admin_settings["button_height"]}px]')
with current_button: with current_button:
with ui.grid(columns='1fr 1fr').classes('w-full h-full py-5 items-start'): with ui.grid(columns='1fr 1fr').classes('w-full h-full py-5 items-start'):
@ -121,7 +120,7 @@ def page_touchscreen():
<path style="fill:#D61E1E;" d="M85.85,60.394c-9.086,7.86-17.596,16.37-25.456,25.456l349.914,349.914 <path style="fill:#D61E1E;" d="M85.85,60.394c-9.086,7.86-17.596,16.37-25.456,25.456l349.914,349.914
c9.086-7.861,17.596-16.37,25.456-25.456L85.85,60.394z"/> c9.086-7.861,17.596-16.37,25.456-25.456L85.85,60.394z"/>
</svg>''' </svg>'''
ui.html(no_photo_svg, sanitize=False) ui.html(no_photo_svg)
with ui.column().classes('' if admin_settings["photos_on_touchscreen"] else 'col-span-2'): with ui.column().classes('' if admin_settings["photos_on_touchscreen"] else 'col-span-2'):
ui.label(current_user.fullname).classes('text-left text-xl text.bold') ui.label(current_user.fullname).classes('text-left text-xl text.bold')
if admin_settings["times_on_touchscreen"]: if admin_settings["times_on_touchscreen"]:

View File

@ -578,13 +578,3 @@ def write_adminsetting(key: str, value):
except KeyError: except KeyError:
print(f"Kein Einstellungsschlüssel {key} vorhanden.") print(f"Kein Einstellungsschlüssel {key} vorhanden.")
def get_admin_list():
adnin_settings = load_adminsettings()
admin_list = load_adminsettings()["admin_user"]
return admin_list.values()
def get_no_time_users_list():
adnin_settings = load_adminsettings()
admin_list = load_adminsettings()["no_time_users"]
return admin_list.values()

View File

@ -38,24 +38,8 @@ class login_mask:
def login(): def login():
nonlocal data nonlocal data
if username.value in get_admin_list(): if username.value == data["admin_user"]:
current_user = user(username.value) if hash_password(password.value) == data["admin_password"]:
if hash_password(password.value) == current_user.password:
if not username.value in get_no_time_users_list():
with ui.dialog() as forward_dialog, ui.card():
ui.label("Wollen Sie den Administrationsbereich oder den Datenbereich aufrufen?")
def admin_area():
app.storage.user['admin_authenticated'] = True
ui.navigate.to('/admin')
def time_area():
app.storage.user['active_user'] = current_user.username
ui.navigate.to(self.target)
with ui.grid(columns=2).classes('justify-center w-full'):
ui.button("Administrationsbereich", on_click=admin_area)
ui.button("Datenbereich", on_click=time_area)
forward_dialog.open()
else:
app.storage.user['admin_authenticated'] = True app.storage.user['admin_authenticated'] = True
ui.navigate.to("/admin") ui.navigate.to("/admin")
else: else:

38
main.py
View File

@ -4,6 +4,7 @@ import os.path
from lib.web_ui import * from lib.web_ui import *
from lib.admin import * from lib.admin import *
from lib.login import *
from lib.users import * from lib.users import *
from lib.touchscreen import * from lib.touchscreen import *
from lib.definitions import * from lib.definitions import *
@ -32,7 +33,7 @@ def main():
list_users() list_users()
#homepage() homepage()
def startup_message(): def startup_message():
@ -53,7 +54,7 @@ def main():
ui.toggle.default_props('rounded') ui.toggle.default_props('rounded')
ui.row.default_classes('items-baseline') ui.row.default_classes('items-baseline')
ui.run(root=homepage, favicon='', port=port, storage_secret=secret, language='de-DE', show_welcome_message=False) ui.run(favicon='', port=port, storage_secret=secret, language='de-DE', show_welcome_message=False)
if __name__ in ("__main__", "__mp_main__"): if __name__ in ("__main__", "__mp_main__"):
parser = argparse.ArgumentParser(description=f'{app_title} {app_version}') parser = argparse.ArgumentParser(description=f'{app_title} {app_version}')
@ -84,34 +85,11 @@ if __name__ in ("__main__", "__mp_main__"):
print("Sollen diese Einstellungen übernommen werden? j=Ja") print("Sollen diese Einstellungen übernommen werden? j=Ja")
question = input() question = input()
if question == "j": if question == "j":
if not os.path.exists(userfolder): admin_settings["admin_user"] = admin_user
os.makedirs(userfolder) admin_settings["admin_password"] = hash_password(admin_password)
print("Kein Ordner mit Benutzerdaten gefunden. Lege ihn an.") json_dict = json.dumps(admin_settings, indent=4)
if not os.path.exists(os.path.join(userfolder, admin_user)): with open(os.path.join(scriptpath, usersettingsfilename), "w") as outputfile:
print("Benutzer existiert noch nicht. Lege ihn an.") outputfile.write(json_dict)
os.makedirs(os.path.join(userfolder, admin_user))
start_date_dt = datetime.datetime.now()
start_date = start_date_dt.strftime("%Y-%m-%d")
settings_to_write = standard_usersettings
settings_to_write["workhours"][start_date] = {}
settings_to_write["fullname"] = "Administrator"
settings_to_write["username"] = admin_user
# API-Key erzeugen
string_to_hash = f'{admin_user}_{datetime.datetime.now().timestamp()}'
hash_string = hashlib.shake_256(bytes(string_to_hash, 'utf-8')).hexdigest(20)
settings_to_write["api_key"] = hash_string
for i in range(1, 8):
settings_to_write["workhours"][start_date][str(i)] = 0
settings_to_write["workhours"][start_date]["vacation"] = 0
with open(f"{userfolder}/{admin_user}/{usersettingsfilename}", 'w') as json_file:
json_dict = json.dumps(standard_usersettings, indent=4)
json_file.write(json_dict)
current_user = user(admin_user)
current_user.password = hash_password(admin_password)
current_user.write_settings()
admin_users_list = load_adminsettings()["admin_user"]
admin_users_list[str(len(admin_users_list))] = admin_user
write_adminsetting("admin_user", admin_users_list)
print("Daten geschrieben") print("Daten geschrieben")
quit() quit()
else: else:

View File

@ -1,20 +0,0 @@
{
"admin_user": {
"0": "admin"
},
"no_time_users": {
"0": "admin"
},
"port": "8090",
"secret": "ftgzuhjikg,mt5jn46uzer8sfi9okrmtzjhndfierko5zltjhdgise",
"times_on_touchscreen": true,
"photos_on_touchscreen": true,
"touchscreen": true,
"picture_height": 200,
"button_height": "300",
"user_notes": true,
"vacation_application": true,
"backup_folder": "/home/alexander/Dokumente/Python/Zeiterfassung/backup",
"backup_api_key": "6fed93dc4a35308b2c073a8a6f3284afe1fb9946",
"holidays": {}
}