Initial Commit für das Grundgerüst der Anwendung, bestehend aus der Dev-Container Konfiguration, der Docker und docker-Compose Definitionen sowie der Verzeichnisse App_Server für die Backend-Programme sowie Client für die browserseitigen GUI-Komponenten

This commit is contained in:
gg committed 2026-08-17 16:10:11 +02:00
1 parent 10549934ea
commit 1ba78992a6
63 files changed
+5410

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
{
"name": "KBP-DevContainer",
"dockerComposeFile": [
"../docker-compose.base.yaml",
"../docker-compose.dev.yaml"
],
"service": "pythonapp",
"workspaceFolder": "/usr/opt/pythonapp",
"shutdownAction": "stopCompose",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
],
"settings": {
"python.defaultInterpreterPath": "/usr/opt/pythonapp/env/bin/python",
"python.linting.enabled": true
}
}
},
"remoteUser": "root"
}
+20
View File
@@ -0,0 +1,20 @@
# Parameter für NGINX-Server
NGINX_VERSION=mainline-alpine
NGINX_HOST_PORT=443
NGINX_SERVER_NAME=kbp_crm.mylocal.de
NGINX_STATIC_DIR=/home/schoschi/Entwicklung/Projekte_Runden/kbcrm/NGINX_Server/html
NGINX_CONFIG_TEMPLATE_DIR=/home/schoschi/Entwicklung/Projekte_Runden/kbcrm/NGINX_Server/configtemplates
NGINX_CERTS=/home/schoschi/Entwicklung/Projekte_Runden/kbcrm/NGINX_Server/certs
NGINX_CERT=wildcard.mylocal.crt
NGINX_CERT_KEY=wildcard.mylocal.key
# Parameter for Python App-Server
PYTHON_VERSION=slim-bookworm
PYTHON_APP_DIR=/home/schoschi/Entwicklung/Projekte_Runden/kbcrm/App_Server/root
PYTHON_DEV_DIR=/home/schoschi/Entwicklung/Projekte_Runden/kbcrm
PYTHON_DB_DIR=/home/schoschi/Entwicklung/Projekte_Runden/kbcrm/App_Server/data
# PYTHON_ENV_DIR=/home/schoschi/Entwicklung/Projekte_Runden/kbcrm/App_Server/env
# Parameter für Dev-Mode (Development-Container für VS-Code)
NODE_VERSION=lts
NPM_VERSION=latest
+6
View File
@@ -0,0 +1,6 @@
__pycache__
env
node_modules
tsout
kbp_crm*.js
App_Server/data/
+103
View File
@@ -0,0 +1,103 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "typescript",
"tsconfig": "Client/tsconfig.json",
"problemMatcher": [
"$tsc"
],
"group": "build",
"label": "Typescript: Compile",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": false
}
},
{
"type": "shell",
"group": "build",
"label": "Rollup: bundle",
"command": "npx rollup --config rollup.config.mjs",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": false
},
"options": {
"cwd": "${workspaceFolder}/Client"
}
},
{
"label": "Build Complete",
"dependsOrder": "sequence",
"dependsOn": [
"Typescript: Compile",
"Rollup: bundle",
"ShowTime"
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
},
{
"type": "shell",
"group": "none",
"label": "ShowTime",
"command": "date",
"presentation": {
"echo": false,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": false
}
},
{
"type": "shell",
"group": "none",
"label": "Open auth.db",
"command": "C:/Daten-GGG/OneDrive/GGG-Workspace/Projekte/Sqlite3/sqlite3.exe auth.db",
"options": {
"cwd": "${workspaceFolder}/App_Server/data"
},
"problemMatcher": [],
"presentation": {
"echo": false,
"reveal": "always",
"focus": true,
"panel": "new",
"showReuseMessage": true,
"clear": true
}
},
{
"type": "shell",
"group": "none",
"label": "Open app.db",
"command": "C:/Daten-GGG/OneDrive/GGG-Workspace/Projekte/Sqlite3/sqlite3.exe kbp_crm.db",
"options": {
"cwd": "${workspaceFolder}/App_Server/data"
},
"problemMatcher": [],
"presentation": {
"echo": false,
"reveal": "always",
"focus": true,
"panel": "new",
"showReuseMessage": true,
"clear": true
}
}
]
}
+4
View File
@@ -0,0 +1,4 @@
*/node_modules
*/tsout
*/env
*/data
+59
View File
@@ -0,0 +1,59 @@
ARG PYTHON_VERSION
ARG NODE_VERSION
ARG NPM_VERSION
FROM python:${PYTHON_VERSION} AS prod
RUN ln -sf /usr/share/zoneinfo/Europe/Berlin /etc/localtime
RUN apt-get clean && apt-get update && apt-get install -y locales
RUN echo "de_DE.UTF-8 UTF-8" >> /etc/locale.gen
RUN locale-gen
# Das Verzeichnis /usr/opt/pythonapp/env wird als Python-Umgebung (venv) verwendet.
# Es wird bewusst außerhalb des Verzeichnisses /usr/opt/pythonapp/App_Server erstellt,
# um die Python-Umgebung von den Anwendungsdateien zu trennen und das Virtual Environment nicht auf dem Host zu zeigen.
RUN mkdir -p /usr/opt/pythonapp/App_Server/root /usr/opt/pythonapp/env /usr/var/pythonappdata
WORKDIR /usr/opt/pythonapp
RUN python -m venv --upgrade-deps /usr/opt/pythonapp/env
COPY root/requirements.txt /usr/opt/pythonapp/App_Server/root/requirements.txt
RUN /usr/opt/pythonapp/env/bin/pip install --no-cache-dir --upgrade pip -r /usr/opt/pythonapp/App_Server/root/requirements.txt
# COPY scripts/init.sh /init.sh
# RUN chmod 552 /init.sh
# # ENTRYPOINT [ "/init.sh" ]
# ENTRYPOINT [ "sh", "/init.sh" ]
# CMD []
FROM prod AS dev
ARG NODE_VERSION
ARG NPM_VERSION
# --- 2. Systemabhängigkeiten (für Python + Node.js) ---
RUN apt-get update && \
apt-get install -y --no-install-recommends \
git \
curl \
procps \
build-essential \
libpq-dev \
libfreetype6-dev \
libjpeg62-turbo-dev \
libxml2-dev \
libxslt1-dev \
sqlite3 \
&& rm -rf /var/lib/apt/lists/*
# --- 3. Node.js + npm installieren ---
RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && \
apt-get install -y nodejs && \
npm install -g npm@${NPM_VERSION}
# --- 8. Arbeitsverzeichnis für TypeScript ---
# In dem Entwicklungsverzeichnis (PYTHON_DEV_DIR) existiert dies schon als Unterverzeichnis "Client", daher wird es hier nicht erstellt
# --- 9. TypeScript + Rollup installieren ---
# Das Script inittypescriptdev.sh wird in der Entwicklungsumgebung ausgeführt, um die TypeScript-Umgebung zu initialisieren.
# Daher ist es nicht notwendig, es hier zu kopieren und auszuführen.
# COPY ./scripts/inittypescriptdev.sh /usr/opt/pythonapp/App_Server/scripts/inittypescriptdev.sh
# RUN chmod +x /usr/opt/pythonapp/App_Server/scripts/inittypescriptdev.sh
ENTRYPOINT ["sh", "/usr/opt/pythonapp/App_Server/scripts/inittypescriptdev.sh"]
CMD ["/usr/opt/pythonapp/env/bin/python", "root/runserver.py", "-D"]
+45
View File
@@ -0,0 +1,45 @@
from flask import Flask
from flask_login import LoginManager
from settings import Configuration
from db import dbauth
from db import dbapp
from auth.model.checkdb import checkupdates as checkupdatesauth
# from model.checkdb import checkupdates as checkupdatesappdb
from os import path
login_manager = LoginManager()
def init_app():
app = Flask(__name__)
config: Configuration = Configuration("root/config.ini")
_projektname = config.app_projectname
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////usr/var/pythonappdb/{}.db".format(_projektname)
app.config["SQLALCHEMY_BINDS"] = {
"auth": "sqlite:////usr/var/pythonappdb/auth.db",
"appdb": "sqlite:////usr/var/pythonappdb/{}.db".format(_projektname)
}
app.config["DEBUG"] = path.exists("root/debug")
app.config["PROJEKTNAME"] = config.app_projectname
app.config["APPVERSION"] = config.clientapp_version
app.config["SECRET_KEY"] = config.flask_secret
# Initialize Plugins
login_manager.init_app(app)
dbauth.init_app(app)
#dbapp.init_app(app)
with app.app_context():
# Include our Routes
print("before auth.routes", flush=True)
from auth import routes
from routes import general
checkupdatesauth()
# checkupdatesappdb()
# from maintenance import init_cli
return app
+258
View File
@@ -0,0 +1,258 @@
import sqlite3
from typing import List
from sqlalchemy.engine import Engine
from sqlalchemy import ForeignKey, event
from sqlalchemy import Column, Integer, String, DateTime
import datetime, decimal
from db import dbauth
SYS_ADMIN = "SysAdmin"
APP_ADMIN = "AppAdmin"
APP_USER = "User"
APP_DOZENT = "Doz"
BINDKEY = 'auth'
#c = Connection()
def columns2dict(record, excludefields=[]):
if not isinstance(excludefields, list):
excludefields = []
attribs = dict()
if hasattr(record, '__table__'):
if hasattr(record.__table__, 'columns'):
for col in record.__table__.columns:
if col.name in excludefields:
continue
value = getattr(record, col.name)
if isinstance(value, datetime.date):
value = value.isoformat()
if isinstance(value, decimal.Decimal):
value = "{0:.2f}".format(value)
attribs[col.name] = value
return attribs
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
# Dieser Trigger darf nur bei einer Sqlite-Verbindung zünden.
# Bei einer VErbindugn zum MS SQL-Server darf er nicht ausgelöst werden
if isinstance(dbapi_connection, sqlite3.Connection):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
class Systeminfo(dbauth.Model):
__tablename__ = 'systeminfo'
__bind_key__ = BINDKEY
id = Column(Integer, primary_key=True)
version = Column(String(20), nullable=False)
lastupdate = Column(DateTime, nullable=True)
initialinstall = Column(DateTime, nullable=True)
# diese Tabelle enthält für alle Apps diejenigen Rollen, die in der App notwendig sind
# Da die Rollen nur einmal in der Tabelle "Role" definiert sind und mehrere Apps die gleichen
# Rollen haben können, handelt es sich im eine "many-to-many"-Relation, die in dieser
# Hilfstabelle verwaltet werden
apps_roles = dbauth.Table(
'apps_roles',
Column('appid', Integer, ForeignKey('applications.id'), primary_key=True),
Column('roleid', Integer, ForeignKey('roles.id'), primary_key=True),
bind_key=BINDKEY
)
# diese Tabelle enthält für alle Apps, in denen der User eine Rolle hat, die Rollen,
# die der User in der jeweiligen App hat.
# Da die Rollen nur einmal in der Tabelle "Role" definiert sind und verschiedene einem User
# zugeordnete Apps die gleichen Rollen haben können, handelt es sich im eine
# "many-to-many"-Relation, die in dieser Hilfstabelle verwaltet werden
userapps_roles = dbauth.Table(
'userapps_roles',
Column('userappid', Integer, ForeignKey('userapps.id'), primary_key=True),
Column('roleid', Integer, ForeignKey('roles.id'), primary_key=True),
bind_key=BINDKEY
)
class Role(dbauth.Model):
__tablename__ = 'roles'
__bind_key__ = BINDKEY
id = Column(Integer, primary_key=True)
roleid = Column(String(80), index=True, unique=True, nullable=False, default=None)
name = Column(String(80), nullable=False, default=None)
level = Column(Integer, default=0)
beschreibung = Column(String(800))
def to_dict(self):
if self.level is None:
self.level = 0
return columns2dict(self)
# Die Tabelle "UserApp" enthält für jeden User für jede App, in der der User eine Rolle hat,
# einen Datensatz.
class Userapp(dbauth.Model):
__tablename__ = 'userapps'
__bind_key__ = BINDKEY
id = Column(Integer, primary_key=True)
userid = Column(ForeignKey('users.id'), index=True)
appid = Column(ForeignKey('applications.id'))
userapp_roles = dbauth.relationship('Role', secondary=userapps_roles, lazy='subquery', order_by="asc(Role.roleid)")
application = dbauth.relationship('Application', uselist=False)
def get_role(self, roleid: str) -> Role:
"""
Die Methode gibt die Rolle aus der userapp zurück, die mit der roleid (nicht id) angegeben wurde.
Gibt es keine Rolle mit der übergebenen roleid, wird 'None' zurückgegeben
- roleid: str
- die roleid (nicht id) der Rollue, die gesucht wird
"""
for role in self.userapp_roles:
if role.id == roleid:
return role
return None
def to_dict(self, withrelations=True):
result = columns2dict(self)
result["userapp_roles"] = []
if withrelations:
for userapprole in self.userapp_roles:
result["userapp_roles"].append(userapprole.to_dict())
return result
class User(dbauth.Model):
__tablename__ = 'users'
__bind_key__ = BINDKEY
id = Column(Integer, primary_key=True)
userid = Column(String(80), index=True, unique=True, nullable=False, default='')
username = Column(String(80), nullable=False)
password = Column(String(40), nullable=False)
email = Column(String(120))
telefon = Column(String(800))
# Jeder User kann mehrere "User-Apps" haben. Dies sind die Apps, in denen dem User Rollen
# zugewiesen wurden. Es handelt sich um eine "one-to-many"-Relation, da ein User zwar mehrere
# User-Apps haben kann, jede User-App aber genau einem User gehört
userapps = dbauth.relationship('Userapp')
def get_userapp(self, appid:str) -> Userapp:
"""
Die Funktion liefert die userapp mit Hilfe der appid (nicht der id) zurück.
Wenn der User keine userapp für die Application hat, wird 'None' zurückgegeben
- appid: string
- Der Kurzname der Application (nicht die id)
"""
for userapp in self.userapps:
app = userapp.application
if app.appid == appid:
return userapp
return None
def get_userapp_by_id(self, id:int) -> Userapp:
"""
Die Funktion liefert die userapp mit Hilfe der id (nicht der appid) zurück
- appid: int
- die interne Record-ID der Application (nicht die userid)
"""
for userapp in self.userapps:
app = userapp.application
if app.id == id:
return userapp
return None
def get_userapproles(self, appid:str) -> List[Role]:
"""
Die Funktion liefert die roles zurück, die der User in der durch die appid (nicht id) bestimmten Application hat
- appid: str
- Der Kurzname der Application
"""
userapp: Userapp = self.get_userapp(appid)
if userapp is not None:
return userapp.userapp_roles
else:
return []
def has_roles(self, roleids: List[str]) -> bool:
"""
Die Funktion liefert 'True' zurück, falls der User in irgendeiner Application eine der Rollen hat, deren roleids
übergeben wurden, ansonsten 'False'
- roleids: List[str]
- eine Liste mit roleids (nicht ids)
"""
for userapp in self.userapps:
for role in userapp.userapp_roles:
if role.roleid in roleids:
return True
return False
def is_SysAdmin(self) -> bool:
"""
die Methode liefert 'True' zurück, wenn der User in irgendeiner Anwendung SysAdmin ist
"""
return self.has_roles([SYS_ADMIN])
def __repr__(self):
return '<User %r>' % self.username
def to_dict(self, withrelations=False):
result = columns2dict(self, excludefields=['password'])
result['userapps'] = []
if withrelations:
for userapp in self.userapps:
result['userapps'].append(userapp.to_dict())
return result
def to_relationaldict(self):
return self.to_dict(withrelations=True)
def has_permission(self, appid:str, allowed_roles: List[str]):
"""
Gibt an, ob der User in einer bestimmten Anwendung eine der gewünschten Rollen hat
- appid: string
- Die appid (nicht id) der gewünschten Application
- allowed_roles: List[str]
- Eine Liste mit den roleids (nicht ids) der erlaubten Rollen
"""
userapp = self.get_userapp(appid)
if userapp is not None:
rolleerlaubt = any([role.roleid in allowed_roles for role in userapp.userapp_roles])
if rolleerlaubt:
return True
return False
# Die nachfolgenden Properties werden für flask_login benötigt
@property
def is_authenticated(self):
return True
@property
def is_active(self):
return True
@property
def is_anonymous(self):
return False
def get_id(self):
return self.userid
class Application(dbauth.Model):
__tablename__ = 'applications'
__bind_key__ = BINDKEY
id = Column(Integer, primary_key=True)
appid = Column(String(80), index=True, unique=True, nullable=False, default=None)
name = Column(String(80), nullable=False, default=None)
minversion = Column(String(20))
beschreibung = Column(String(800))
roles = dbauth.relationship('Role', secondary=apps_roles, lazy='subquery', order_by="asc(Role.roleid)")
def to_dict(self, withrelations=True):
result = columns2dict(self)
result['roles'] = []
for role in self.roles:
result['roles'].append(role.to_dict())
return result
print("########## auth/model importiert")
@@ -0,0 +1,73 @@
from . import APP_ADMIN, SYS_ADMIN, User, Userapp, dbauth, Application, Role, apps_roles, userapps_roles
from .errors import AutorisationError, ApplicationError
from flask_login import current_user
from sqlalchemy import delete
session = dbauth.session
def get_applications():
apps = []
for app in Application.query:
# Die Application wird nur zurückgegeben, wenn der current-User SysAdmin ist oder wenn
# der current_user in der zurückzugebenden Application AppAdmin ist
if current_user.is_SysAdmin() or current_user.has_permission(app.appid, [APP_ADMIN]):
if not current_user.is_SysAdmin():
for role in list(app.roles):
if role.roleid == SYS_ADMIN:
app.roles.remove(role)
apps.append(app)
return apps
def updateapplication(data):
if not current_user.is_SysAdmin():
raise AutorisationError(allowed_roles=[SYS_ADMIN], user_roles=current_user.get_userapproles(data["appid"]))
currentapp = None
if data["id"] == -1:
currentapp = Application()
currentapp.id = None
session.add(currentapp)
else:
currentapp = Application.query.get(data["id"])
if currentapp is None:
raise Exception ("404")
currentapp.name = data["name"]
currentapp.appid = data["appid"]
currentapp.minversion = data["minversion"]
currentapp.beschreibung = data["beschreibung"]
session.commit()
def deleteapplications(appids):
if not current_user.is_SysAdmin():
raise AutorisationError(allowed_roles=[SYS_ADMIN], user_roles=None)
for appid in appids:
app: Application = Application.query.filter(Application.appid == appid).one()
if app is not None:
# Die Datensätze in "apps_roles" müssen gelöscht werden
app.roles.clear()
# UserApps löschen
q_userapps = dbauth.select(Userapp). where(Userapp.appid == app.id)
for userapp in dbauth.session.scalars(q_userapps):
userapp.userapp_roles.clear()
session.delete(userapp)
session.delete(app)
session.commit()
def updateapplicationroles(approles):
# Änderungen an Applications darf grundsätzlich nur der SysAdmin machen
if not current_user.is_SysAdmin():
raise AutorisationError(allowed_roles=[SYS_ADMIN], user_roles=None)
for appidinfo in approles:
app: Application = Application.query.filter(Application.appid == appidinfo["appid"]).one()
if app is None:
raise ApplicationError(f"Die Application mit der Appid {appidinfo['appid']} existiert nicht", )
for roleidinfo in appidinfo["roles"]:
role: Role = Role.query.filter(Role.roleid == roleidinfo["roleid"]).one()
if role in app.roles:
if not roleidinfo['isChecked']:
app.roles.remove(role) # hier kann das remove einfach so verwendet werden, da nicht über app.roles iteriert wird
# TODO: die Rolle muss auch aus allen userapps für alle User gelöscht werden
else:
if roleidinfo['isChecked']:
app.roles.append(role)
session.commit()
+86
View File
@@ -0,0 +1,86 @@
from datetime import datetime
from auth import model
from sqlalchemy import text
from sqlalchemy.exc import OperationalError
from datetime import datetime
from db import dbauth
session = dbauth.session
BINDKEY = "auth"
def _db_V01_00():
# "bind_key=None" bedeutet hier, dass bei "create_all" nur die Models mit
# Default-Bind erzeugt werden sollen und nicht die anderen, insbesonder
# nicht die zum MS-SQL-SErver
dbauth.create_all(bind_key=BINDKEY)
systeminfo = model.Systeminfo()
systeminfo.id = 1
systeminfo.version = "01.00"
systeminfo.lastupdate = datetime.now()
systeminfo.initialinstall = datetime.now()
session.add(systeminfo)
session.commit()
def _db_V01_01():
u = model.User()
u.id = 1
u.userid = 'System.Admin'
u.username = 'Systemadministrator'
u.password = "geheim"
u.email = ''
session.add(u)
sysmgr = model.Role()
sysmgr.id = 1
sysmgr.name = "Systemadministrator"
sysmgr.roleid = "SysAdmin"
session.add(sysmgr)
appmgr = model.Role()
appmgr.name = "Anwendungsadministrator"
appmgr.roleid = "AppAdmin"
session.add(appmgr)
usermgr = model.Role()
usermgr.name = "Benutzerverwaltung"
usermgr.roleid = "UserMgr"
session.add(usermgr)
application = model.Application()
application.id = 1
application.appid = "Project01"
application.name = "Project01 (Name ändern)"
session.add(application)
userapp = model.Userapp()
userapp.application = application
userapp.userapp_roles.append(sysmgr)
session.add(userapp)
u.userapps.append(userapp)
_updatesysteminfo('01.01')
session.commit()
def _updatesysteminfo(versionnr):
s: model.Systeminfo = model.Systeminfo.query.get(1)
s.version = versionnr
s.lastupdate = datetime.now()
return
def checkupdates():
systeminfo = None
curr_dbversion = '_db_V01_00'
try:
systeminfo: model.Systeminfo = model.Systeminfo.query.get(1)
curr_dbversion = "_db_V" + systeminfo.version.replace(".", "_")
except (OperationalError, AttributeError):
_db_V01_00()
versionen = sorted(filter(lambda f: f.startswith("_db_V"),globals()))
for v in versionen:
if v > curr_dbversion:
globals()[v]() # https://docs.python.org/3.6/library/functions.html?highlight=globals#globals
print('########## Database updated')
+38
View File
@@ -0,0 +1,38 @@
class ApplicationError(Exception):
def __init__(self, *messages, htmlstatus=400):
self.htmlstatus = htmlstatus
Exception.__init__(self, *messages)
@property
def error(self):
error = {
"status": self.htmlstatus,
"messages": list(self.args)
}
return error
class AutorisationError(ApplicationError):
def __init__(self, allowed_roles=[], user_roles=[]):
self.allowed_roles = allowed_roles
self.user_roles = user_roles
if user_roles is None:
userrolestxt = "NoSysAdmin"
else:
userrolestxt = ", ".join([role.name for role in self.user_roles])
ApplicationError.__init__(self,
"Sie haben nicht die passende Rolle für diese Funktion",
"Ihre Rollen sind: " + userrolestxt,
"Erforderliche Rollen sind: " + ", ".join(self.allowed_roles), htmlstatus=HtmlStatusCode.Unauthorized)
class HtmlStatusCode():
Bad_Request = 400
Unauthorized = 401
Payment_Required = 402
Forbidden = 403
Not_Found = 404
Method_Not_Allowed = 405
+50
View File
@@ -0,0 +1,50 @@
from . import SYS_ADMIN, Application, Role, dbauth, User
from sqlalchemy.sql import collate
from flask_login import current_user
from .errors import AutorisationError
session = dbauth.session
def get_roles():
roles = Role.query.order_by(collate(Role.roleid, 'NOCASE')).all()
return roles
def updaterole(data):
if not current_user.is_SysAdmin():
raise AutorisationError(allowed_roles=[SYS_ADMIN], user_roles=None)
currentrole = None
if data["id"] == -1:
currentrole = Role()
currentrole.id = None
session.add(currentrole)
else:
currentrole = Role.query.get(data["id"])
if currentrole is None:
raise Exception ("404")
currentrole.level = data["level"]
currentrole.name = data["name"]
currentrole.roleid = data["roleid"]
currentrole.beschreibung = data["beschreibung"]
session.commit()
def deleteroles(roleids):
if not current_user.is_SysAdmin():
raise AutorisationError(allowed_roles=[SYS_ADMIN], user_roles=None)
for roleid in roleids:
role: Role = Role.query.filter(Role.roleid == roleid).one()
if role is not None:
appq = Application.query
for app in appq:
if role in app.roles:
app.roles.remove(role) # remove kann hier einfach so verwendet werden, da nicht über app.roles iteriert wird
userq = User.query
for user in userq:
for userapp in user.userapps:
if role in userapp.userapp_roles:
userapp.userapp_roles.remove(role) # remove kann hier einfach so verwendet werden, da nicht über userapp_roles iteriert wird
session.delete(role)
session.commit()
+179
View File
@@ -0,0 +1,179 @@
from . import APP_ADMIN, SYS_ADMIN, Application, Role, User, dbauth, Userapp
from .errors import ApplicationError, AutorisationError
from flask_login import current_user
current_user: User
session = dbauth.session
def check_login(userid: str, password: str):
"""
Hier wird ausschließlich nur festgestellt, ob Login und Kennwort vorhanden sind bzw. übereinstimmen.
alles Weitere muss in den Funktionen erfolgen, die 'check_login' aufrufen.
"""
user:User = User.query.filter(User.userid == userid).first()
if user is None or user.password != password:
return None
else:
return user
def get_users():
"""
Hier werden derzeit alle User zurückgegeben.
Allerdings werden die userapps eines Users wie folgt gefiltert:
Wenn es sich bei dem current_user um den SysAdmin handelt, wird nichts gefiltert
Wenn es sich bei dem current_user um den AppAdmin handelt, werden nur die userapps zurückgegeben, auf die auch
der current_user Zugriff hat. Außerdem wird die Rolle "SysAdmin" innerhalb der userapp.userapp-roles auch nicht
mit zurückgegeben
Bei allen anderen Arten von Usern werden keine userapps und somit auch keine Rollen zurückgegeben
"""
users = []
user: User
for user in User.query.order_by(User.userid):
# Wenn der current_user kein SysAdmin ist, soll diese User auch nicht sehen, dass es einen SysAdmin gibt
if not current_user.is_SysAdmin() and user.is_SysAdmin():
continue
# Wenn innerhalb einer For-Schleife Elemente gelöscht werden, muss auf einer Kopie der
# Liste iteriert werden, da die For-Schleife sonst mit der Nummerierung durcheinanderkommt, weil während
# des Durchlaufs Werte durch das remove "verschwinden"
# Daher wird hier für die For-Schleife mit der Funktion list() eine neue Liste der Origonal-Liste erzeugt
for userapp in list(user.userapps):
# Rollen für bestimmte Application, in denen der zurückzugebende User Rollen innehat, sollen nur zurück-
# gegeben werden, wenn der current_user für diese App SysAdmin oder AppAdmin ist
if not current_user.has_permission(userapp.application.appid, [APP_ADMIN, SYS_ADMIN]):
user.userapps.remove(userapp)
# Wenn der current_user für die Application keine SysAdmin-Rolle hat, soll eine evtl. SysAdmin-Rolle auch
# nicht zurückgegeben werden
if not current_user.has_permission(userapp.application.appid, [SYS_ADMIN]):
for role in list(userapp.userapp_roles): # s.o. wegen remove in for-loop
if role.roleid == SYS_ADMIN:
userapp.userapp_roles.remove(role)
users.append(user)
return users
def changepassword(userid:str, oldpassword:str, newpassword1:str, newpassword2:str):
"""
Die Funktion ändert das Kennwort des Users
-userid: string Die Userid (nicht id) des Users
-oldpassword: string Das bisherige Kennwort des Users
-newpassword1: string Das neue Kennwort des Users
-newpassword2: string Die Wiederholung des neuen Kennwort des Users
"""
if newpassword1 != newpassword2:
raise ValueError("Die neuen Kennworte stimmen nicht überein!")
if newpassword1 is None or newpassword1.strip() == "":
raise ValueError("Es muss ein neues Kennwort angegeben werden!")
user:User = User.query.filter(User.userid == userid).first()
if user is None or user.password != oldpassword:
raise ValueError("Der angegebene Benutzer existiert nicht oder das alte Kennwort ist falsch")
user.password = newpassword1
session.commit()
return True
def updateuser(data):
"""
Die User-Daten werden nur geändert, wenn der current_user entweder SysAdmin oder AppAdmin ist
-data: Dict Ein Dictionary mit den Feldern, die beim User geändert werden sollen
"""
if not current_user.has_roles([SYS_ADMIN, APP_ADMIN]):
raise AutorisationError(allowed_roles=[SYS_ADMIN, APP_ADMIN], user_roles=None)
currentuser: User = None
if data["id"] == -1:
currentuser = User()
currentuser.id = None
currentuser.password = ""
session.add(currentuser)
else:
currentuser = User.query.get(data["id"])
if currentuser is None:
raise Exception ("404")
currentuser.username = data["username"]
currentuser.userid = data["userid"]
currentuser.email = data["email"]
currentuser.telefon = data["telefon"]
session.commit()
def deleteusers(userids):
"""
Hier werden die User gelöscht, deren userids übergeben werden. Gelöscht werden kann aber nur,
wenn der current_user entweder SysAdmin oder AppAdmin ist.
Wenn der zu löschende User Rollen in Applications hat, auf die der AppAdmin keinen Zugriff hat,
Wird der User ebenfalls nicht gelöscht.
- userids: string[] eine Liste der userids (nicht id) der User, die gelöscht werden sollen
"""
# Wenn er schon mal nirgendwo Sys- oder AppAdmin ist, fliegt er schon mal direkt raus
if not current_user.has_roles([SYS_ADMIN, APP_ADMIN]):
raise AutorisationError(allowed_roles=[SYS_ADMIN, APP_ADMIN], user_roles=None)
# ansonsten: hier dürfen nur die User gelöscht werden, die in denjenigen Apps eine Rolle haben,
# in dene der current_user entweder SysAdmin aod AppAdmin ist. Wenn der User noch eine Rolle in einer App haben,
# auf die der current_user keinen Zugriff hat, darf nicht gelöscht werden
for userid in userids:
user: User = User.query.filter(User.userid == userid).one()
if user is not None:
for userapp in user.userapps:
currentuser_userapp: Userapp = current_user.get_userapp(userapp.application.appid)
if currentuser_userapp is None or len(currentuser_userapp.userapp_roles) == 0:
raise ApplicationError(f"Der Benutzer {user.username} hat noch Rollen in Anwendungen, in denen Sie kein AppAdmin oder SysAdmin sind.",
"Er kann daher nicht gelöscht werden.",
"Der komplette Löschvorgang wurde abgebrochen.",
htmlstatus=404)
session.delete(user)
session.commit()
def applyuserapprole(data):
"""
Hier kann einem Benutzer eine Rolle für eine bestimmte App entzogen oder zugewiesen werden
### Parameter
- data: dict
- parentuserid: integer
- Die id (nicht userid) des Users, für den eine Rolle geändert werden soll
- parantappid: integer
- Die id (nicht appid) der Application, für die eine Rolle geändert werden soll
- roleid: integer
- Die id (nicht roleid) der Rolle, die zugewiesen oder entzogen werden soll
- ischecked: boolean
- 'True' bedeutet, dass die Rolle zugewiesen werden soll, 'False' bedeutet, dass die Rolle entzogen werden soll
"""
user_id = data['parentuserid']
app_id = data['parentappid']
ischecked = data['ischecked']
role_id = data['roleid']
user: User = User.query.get(user_id)
if user is None:
raise Exception("Den User gibt es nicht")
app: Application = Application.query.get(app_id)
if app is None:
raise Exception("Die Application gibt es nicht")
role: Role = Role.query.get(role_id)
if role is None:
raise Exception("Die Rolle gibt es nicht")
# hier darf nur zugewiesen werden, wenn der current_user entweder SysAdmin ist oder AppAdmin in der App,
# für die die Rolle geändert wird
if not current_user.is_SysAdmin() and not current_user.has_permission(app.appid, [APP_ADMIN]):
raise AutorisationError(allowed_roles=[SYS_ADMIN, APP_ADMIN], user_roles=current_user.get_userapproles(app.appid))
userapp: Userapp = user.get_userapp_by_id(app_id)
if userapp is None:
userapp = Userapp()
userapp.appid = app_id
user.userapps.append(userapp)
userapp_role: Role = userapp.get_role(role_id)
if userapp_role is None and ischecked:
userapp.userapp_roles.append(role)
if userapp_role is not None and not ischecked:
# eine SysAdmin oder ein AppAdmin kann sich selber nicht die Rolle als SysAdmin oder AppAdmin entziehen
if current_user.userid == user.userid:
if ((userapp_role.roleid == APP_ADMIN and current_user.has_permission(app.appid, [APP_ADMIN])) or
(userapp_role.roleid == SYS_ADMIN and current_user.has_permission(app.appid, [SYS_ADMIN]))):
raise ApplicationError("Sie können sich nicht selbst die Rolle als AppAdmin entziehen, da Sie dann keinen Zugriff auf das System mehr hätten!")
userapp.userapp_roles.remove(userapp_role) # remove kann hier einfach so verwendet werden, da nicht über userapp.userapp_roles iteriert wird
session.commit()
+235
View File
@@ -0,0 +1,235 @@
from approot import login_manager
from auth.model import (
APP_ADMIN,
APP_DOZENT,
APP_USER,
SYS_ADMIN,
Application,
Role,
User,
applications,
roles,
users,
)
from flask import current_app as app
from flask import jsonify, request
from flask_login import login_required, login_user, logout_user
from .model.errors import ApplicationError, AutorisationError
print("auth-routes")
@login_manager.user_loader
def user_loader(userid):
user: User | None = User.query.filter(User.userid == userid).first()
return user
@app.post("/auth/checklogin")
def checklogin():
"""
Hier wird nur die Authentifizierung überprüft.
Evtl. Autorisierungen anhand der User-Rollen und App-Berechtigungen müssen später bei den
Funktionsaufrufen erfolgen
"""
data = request.json
user: User = users.check_login(data["userid"], data["password"])
if user is None:
error = ApplicationError(
"Login oder Kennwort stimmen nicht!",
"Anmeldung wurde verweigert",
htmlstatus=403,
)
return jsonify(error.error), error.htmlstatus
if not user.has_roles([SYS_ADMIN, APP_ADMIN, APP_USER, APP_DOZENT]):
error = ApplicationError(
"Sie haben nicht die passende Rolle zur Nutzung dieser Anwendung.",
"Der Zugang wird verweigert",
htmlstatus=403,
)
return jsonify(error.error), error.htmlstatus
login_user(user)
result = user.to_dict()
result["isSysAdmin"] = user.is_SysAdmin()
return jsonify(result), 200
@app.post("/auth/changepassword")
@login_required
def changepassword():
data = request.json
status = 200
result = {"Message": "", "Success": True}
try:
users.changepassword(
data["Userid"],
data["OldPassword"],
data["NewPassword1"],
data["NewPassword2"],
)
except Exception as ex:
error = ApplicationError(str(ex), htmlstatus=400)
return error.error, error.htmlstatus
return jsonify(result), status
@app.get("/auth/logout")
def logout():
logout_user()
return jsonify({"logout": True}), 200
@app.get("/auth/getusers")
@login_required
def getusers():
"""
Die Rückgabe wird in 'users.get_users()' (s.u.) anhand er App-Berechtigung und der
Rollen gefiltert.
"""
userquery = users.get_users()
result = []
user: User
for user in userquery:
result.append(user.to_relationaldict())
return jsonify(result), 200
@app.get("/auth/getapplications")
@login_required
def getapplications():
"""
Die Rückgabe wird in 'applications.get_applications()' (s.u.) anhand er App-Berechtigung und der
Rollen gefiltert.
"""
appquery = applications.get_applications()
result = []
app: Application
for app in appquery:
result.append(app.to_dict())
return jsonify(result), 200
@app.get("/auth/getroles")
@login_required
def getroles():
rolelist = roles.get_roles()
result = []
role: Role
for role in rolelist:
result.append(role.to_dict())
return jsonify(result), 200
@app.post("/auth/updaterole")
@login_required
def updaterole():
data = request.json
try:
roles.updaterole(data)
except AutorisationError as ex:
return jsonify(ex.error), ex.htmlstatus
except Exception as ex:
if str(ex) == "404":
return jsonify("Rolle nicht gefunden"), 404
else:
return jsonify(str(ex)), 400
return jsonify({}), 200
@app.post("/auth/deleteroles")
@login_required
def deleteroles():
data = request.json
try:
roles.deleteroles(data)
except AutorisationError as ex:
return jsonify(ex.error), ex.htmlstatus
except Exception as ex:
return jsonify(str(ex)), 400
return jsonify({}), 200
@app.post("/auth/updateapplication")
@login_required
def updateapplication():
data = request.json
try:
applications.updateapplication(data)
except AutorisationError as ex:
return jsonify(ex.error), ex.htmlstatus
except Exception as ex:
return jsonify(str(ex)), 400
return jsonify({}), 200
@app.post("/auth/deleteapplications")
@login_required
def deleteapplications():
data = request.json
try:
applications.deleteapplications(data)
except AutorisationError as ex:
return jsonify(ex.error), ex.htmlstatus
except Exception as ex:
return str(ex), 400
return {}, 200
@app.post("/auth/updateapplicationroles")
@login_required
def updateapplicationroles():
data = request.json
try:
applications.updateapplicationroles(data)
except AutorisationError as ex:
return jsonify(ex.error), ex.htmlstatus
except Exception as ex:
return jsonify(str(ex)), 400
return jsonify({}), 200
@app.post("/auth/updateuser")
@login_required
def updateuser():
data = request.json
try:
users.updateuser(data)
except AutorisationError as ex:
return jsonify(ex.error), ex.htmlstatus
except Exception as ex:
if str(ex) == "404":
return jsonify("Benutzer nicht gefunden"), 404
else:
return jsonify(str(ex)), 400
return jsonify({}), 200
@app.post("/auth/applyuserapprole")
@login_required
def applyuserapprole():
data = request.json
try:
users.applyuserapprole(data)
except AutorisationError as ex:
return jsonify(ex.error), ex.htmlstatus
except Exception as ex:
return jsonify(str(ex)), 400
return jsonify({}), 200
@app.post("/auth/deleteusers")
@login_required
def deleteusers():
data = request.json
try:
users.deleteusers(data)
except ApplicationError as ex:
return jsonify(ex.error), ex.htmlstatus
except Exception as ex:
return jsonify(str(ex)), 400
return {}, 200
print("########## auth.routes importiert")
+7
View File
@@ -0,0 +1,7 @@
[DEBUG]
debug=True
[APP]
projectname=kbp_crm
version=00.00.01
flasksecret=Hier_muss_ein_sicherer_schluessel_eingesetzt_werden
+10
View File
@@ -0,0 +1,10 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
pass
dbauth = SQLAlchemy(model_class=Base)
dbapp = dbauth
+2
View File
@@ -0,0 +1,2 @@
Wenn diese Datei vorhanden ist, dann startet der Python/Flask-Server im DEBUG-Modus
Wenn die Datei nicht vorhanden ist, startet Python/Flask nicht im DEBUG-Modus
+6
View File
@@ -0,0 +1,6 @@
SQLAlchemy
Flask
Flask-SQLAlchemy
Flask-Login
waitress
# pyodbc
+33
View File
@@ -0,0 +1,33 @@
from flask import current_app as app, render_template, jsonify
from datetime import datetime
from flask_login import login_required
@app.route("/")
@app.route("/index.html")
def index():
scriptfile = "{0}_{1}.js".format(app.config["PROJEKTNAME"], app.config["APPVERSION"])
if app.config["DEBUG"] is False:
scriptfile = scriptfile.replace(".js", ".min.js")
return render_template(
"index.html",
apptitle = app.config["PROJEKTNAME"],
scriptfile = scriptfile
)
@app.route("/xhr/health-check")
def health_check():
zeit = datetime.now().isoformat()
result = {
'result': "success",
'time': zeit,
'server': "n/a", #_server,
'projectname': "n/a", #_projektname,
'clientappversion': "N/A" #_version
}
return result # https://flask.palletsprojects.com/en/3.0.x/quickstart/#about-responses
@app.route("/xhr/getinfo")
@login_required
def get_info():
return {"info": "pustekuchen"}
+20
View File
@@ -0,0 +1,20 @@
from waitress import serve
from werkzeug import serving
from approot import init_app
def run_server():
try:
if app.config["DEBUG"] is False:
_server = "waitress"
# serve(app, listen='nginx-proxy:8080') # Funktioniert aus unbekannten Gründen nicht. Der Hostname wird im Docker Container nicht korrekt aufgelöst
serve(app, listen='*:8080')
else:
_server = "werkzeug"
serving.run_simple("0.0.0.0", 8080, app, use_reloader=True)
except Exception as ex:
print(str(ex))
if __name__ == '__main__':
app = init_app()
run_server()
+26
View File
@@ -0,0 +1,26 @@
from pathlib import Path
from configparser import ConfigParser
class Configuration:
def __init__(self, configfilename:str|Path):
self.config:ConfigParser = ConfigParser()
self.config.read(configfilename)
@property
def debug(self) -> bool:
return self.config["DEBUG"].getboolean("debug")
@property
def clientapp_version(self) -> str:
return self.config["APP"].get("version")
@property
def app_projectname(self) -> str:
return self.config["APP"].get("projectname")
@property
def flask_secret(self) -> str:
return self.config["APP"].get("flasksecret")
+54
View File
@@ -0,0 +1,54 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{apptitle}}</title>
<meta name="description" content="A simple HTML5 Template for new projects.">
<meta name="author" content="SitePoint">
<meta name="clientappversion" content="{{clientappversion}}"/>
<!--
<meta property="og:title" content="A Basic HTML5 Template">
<meta property="og:type" content="website">
<meta property="og:url" content="https://www.sitepoint.com/a-basic-html5-template/">
<meta property="og:description" content="A simple HTML5 Template for new projects.">
<meta property="og:image" content="image.png">
-->
<link rel="icon" href="/favicon.ico">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" id="apptheme" href="static/app-theme.css">
<link rel="stylesheet" href="static/w3.css">
<link rel="stylesheet" href="static/symbols.css">
<!--
<link rel="stylesheet" href="static/fonts.css">
<link rel="stylesheet" href="static/dpa_theme.css">
-->
<style>
/*
body {
font-family: "Lucida Console", bold, sans-serif;
}
*/
/****************************/
/* Individuelle Anpassungen */
/****************************/
/*
body {
font-family: "Lucida Console", bold, sans-serif;
}
*/
</style>
</head>
<body>
<div id="body" />
<script src="static/{{scriptfile}}"></script>
</body>
</html>
+16
View File
@@ -0,0 +1,16 @@
# Python virtual Environment anlegen
cd /usr/opt/pythonapp
# Wenn es die Datei pyvenv.cfg im Unterverzeichnis env nocht nicht gibt, muss das
# virtuelle Environment angelegt werden
# if [ ! -s env/pyvenv.cfg ]
# then
# echo "Creating Python Virtual environment..."
# python -m venv --upgrade-deps /usr/opt/pythonapp/env
# echo "Python Virtual environment created."
# fi
echo "Installing Python requirements..."
env/bin/pip install -r root/requirements.txt
echo "Python requirements installed."
echo "Starting Python-Server..."
env/bin/python root/runserver.py
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# Prüfe, ob node_modules bereits existiert und ob es nicht leer ist
# Das ist wichtig, da das node_modules durch ein bind mount in den Container eingebunden wird
# und daher bei Start des Containers bereits existiert
if [ ! -d "/usr/opt/pythonapp/Client/node_modules" ] || [ -z "$(ls -A /usr/opt/pythonapp/Client/node_modules)" ]; then
echo "node_modules nicht gefunden oder leer. Führe npm install aus..."
cd /usr/opt/pythonapp/Client || { echo "Fehler: /usr/opt/pythonapp/Client nicht gefunden"; exit 1; }
# Prüfe, ob package.json existiert
if [ -f "package.json" ]; then
echo "package.json gefunden. Installiere Abhängigkeiten..."
npm install
echo "npm install abgeschlossen."
else
echo "Fehler: package.json in /usr/opt/pythonapp/Client nicht gefunden!"
exit 1
fi
fi
# Führe das eigentliche CMD aus (z. B. python src/mein_skript.py)
# Zuerst wieder in die Python-Umgebung zurück, da von dort die Anwendung gestartet wird
cd /usr/opt/pythonapp/App_Server || { echo "Fehler: /usr/opt/pythonapp/App_Server nicht gefunden"; exit 1; }
echo "Starte Anwendung..."
exec "$@"
+616
View File
@@ -0,0 +1,616 @@
import m from "mithril"
import { Treetable, iTreetable, iTreenode, Treenode, TopbarButton, iTopbarButton, changeTreenodes } from "../widgets/Treetable_V2"
import {ModalType, ModalView, iModalView} from "../widgets/Modal"
import { iApplication} from "./models/applications"
import { iRole } from "./models/roles"
import {gridMountpoints} from "../mainlayout"
import {WrongEditSelectionMessage, iWrongEditSelectionMessage, showServerError} from "./authmessages"
const MOVERIGHT = "Die markierten und nicht markierten Rollen werden den markierten Anwendungen zugeordnet"
const localDataModel = {
applist: [] as iApplication[],
rolelist: [] as iRole[],
appnodes: [] as iTreenode[],
rolenodes: [] as iTreenode[],
loadData: () => {
Promise.all([
m.request({
method: "GET",
url: "/auth/getapplications"
}).then((result) => localDataModel.applist = result as iApplication[]),
m.request({
method: "GET",
url: "/auth/getroles"
}).then((result) => {localDataModel.rolelist = result as iRole[]})
])
.then (() => {
localDataModel.appnodes = []
localDataModel.rolenodes = []
localDataModel.applist.map((app: iApplication) => {
let appnode: iTreenode = new Treenode(app, false, [])
app.roles.forEach((role: iRole) => {
let newChildnode = new Treenode(role)
newChildnode.isChecked = true
appnode.children.push(newChildnode)
})
appnode.children.sort(sortRoleNodes)
localDataModel.appnodes.push(appnode)
})
localDataModel.rolelist.map((role: iRole) => {
let rolenode: iTreenode = new Treenode(role, false, [])
localDataModel.rolenodes.push(rolenode)
})
localDataModel.rolenodes.sort(sortRoleNodes)
changeTreenodes(localDataModel.appnodes, true)
})
},
updateRole: (role: iRole) => {
m.request ({
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
url: "/auth/updaterole",
body: role
})
.catch((e) => {showServerError(e)})
.finally(() => localDataModel.loadData())
},
deleteRoles: (roles: iRole[]) => {
let roleids: string[] = []
roles.forEach((role: iRole) => {
roleids.push(role.roleid)
})
m.request({
method: "POST",
url: "/auth/deleteroles",
body: roleids
})
.catch((e) => {showServerError(e)})
.finally(() => localDataModel.loadData())
},
updateApplication: (app: iApplication) => {
m.request ({
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
url: "/auth/updateapplication",
body: app
})
.catch((e) => {showServerError(e)})
.finally(() => localDataModel.loadData())
},
deleteApplications: (apps: iApplication[]) => {
let appids: string[] = []
apps.forEach((app: iApplication) => {
appids.push(app.appid)
})
m.request({
method: "POST",
url: "/auth/deleteapplications",
body: appids
})
.catch((e) => {showServerError(e)})
.finally(() => localDataModel.loadData())
},
updateApplicationRoles: () => {
interface iApplicationRole {
appid: string
roles: {roleid: string, isChecked: boolean}[]
}
let applicationroles: iApplicationRole[] = []
localDataModel.appnodes.forEach((appnode: iTreenode) => {
let app: iApplication = appnode.nodevalue
let approle: iApplicationRole = {
appid: app.appid,
roles: []
}
appnode.children.forEach((rolenode: iTreenode) => {
let role: iRole = rolenode.nodevalue
approle.roles.push({roleid: role.roleid, isChecked: rolenode.isChecked})
})
applicationroles.push(approle)
})
m.request({
method: "POST",
url: "/auth/updateapplicationroles",
body: applicationroles
})
.catch((e) => {showServerError(e)})
.finally(() => localDataModel.loadData())
}
}
export const applicationpage: m.ClosureComponent = (vnode) => {
return {
oninit: (vnode) => {
localDataModel.loadData()
},
view: (vnode) => {
return m("div.w3-container.w3-content", {
style: {'max-width': '80%', display: 'flex', 'flex-direction': 'column', height: 'inherit', 'overflow-y': 'inherit'}
},
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
m("div.w3-cell-row",
m("div.w3-cell.w3-padding.w3-mobile",
m("h1", "Rollen"),
m(Treetable,
{
treenodes: localDataModel.rolenodes,
startexpanded: false,
onRowMarked: (node: iTreenode) => {
//alert(node.nodevalue['roleid'])
},
columnmapper: (node: iTreenode) => {
let role = node.nodevalue as iRole
return [
m("span.w3-button",
{
style: {padding: '0px'},
onclick: () => {
node.isChecked = !node.isChecked
}
},
m("i.material-icons.w3-theme-light", node.isChecked?"check_box":"check_box_outline_blank")
),
m("div", role.level),
m("div", role.roleid),
m("div", role.name)
]
}
} as iTreetable,
m(TopbarButton,
{
onclick: applyRoles,
tooltip: MOVERIGHT
} as iTopbarButton,
m("i.material-icons", "keyboard_double_arrow_right")
),
m(TopbarButton,
{
onclick: () => {m.mount(gridMountpoints.modalhook, {
view: (vnode) => {
return m(RoleForm, {role: {id: -1, level: 0, name: "", roleid: ""} as iRole})
}
})}
} as iTopbarButton,
m("i.material-icons", "add")
),
m(TopbarButton,
{
onclick: () => {m.mount(gridMountpoints.modalhook, DeleteRolesMessage)}
} as iTopbarButton,
m("i.material-icons", "delete_forever")
),
m(TopbarButton,
{
onclick: () => {
let roles: iRole[] = []
localDataModel.rolenodes.forEach((rolenode: Treenode) => {
if (rolenode.isChecked) {
roles.push(rolenode.nodevalue)
}
})
if (roles.length != 1) {
m.mount(gridMountpoints.modalhook, WrongRoleEditSelectionMessage)
}
else {
m.mount(gridMountpoints.modalhook, {
view: (vnode) => {
return m(RoleForm, {
role: roles[0] as iRole
})
}
})
}
}
} as iTopbarButton,
m("i.material-icons", "edit")
)
)
),
m("div.w3-cell.w3-padding.w3-mobile",
m("h1", "Anwendungen"),
m(Treetable,
{
treenodes: localDataModel.appnodes,
startexpanded: true,
columnmapper: (node: iTreenode) => {
let cols: any[] = []
// Hier wird "Duck-Typing" genutzt, um den Typ von node.nodevalue zu ermitteln
// Je nach Typ werden die Spalten der Tabelle dann anders befüllt
if (node.nodevalue['appid'] != undefined) {
let app = node.nodevalue as iApplication
cols[0] = "enabled"
cols[1] = app.appid
cols[2] = app.name
}
else if (node.nodevalue['roleid'] != undefined) {
let role = node.nodevalue as iRole
cols[0] = "disabled"
cols[1] = role.roleid
cols[2] = role.name
}
return [
m(cols[0] == "enabled"?"span.w3-button":"span",
{
style: {padding: '0px'},
onclick: () => {
if (cols[0] == "enabled") {
node.isChecked = !node.isChecked
}
}
},
m("i.material-icons.w3-theme-light", node.isChecked?"check_box":"check_box_outline_blank")
),
m("div", cols[1]),
m("div", cols[2])
]
}
} as iTreetable,
m(TopbarButton,
{
onclick: applyRoles,
tooltip: MOVERIGHT
} as iTopbarButton,
m("i.material-icons", "keyboard_double_arrow_right")
),
m(TopbarButton,
{
onclick: () => {m.mount(gridMountpoints.modalhook,
{
view: (vnode) => {
return m(ApplicationForm, {application: {id: -1} as iApplication} as iApplicationForm)
}
}
)}
} as iTopbarButton,
m("i.material-icons", "add")
),
m(TopbarButton,
{
onclick: () => {m.mount(gridMountpoints.modalhook, DeleteApplicatonsMessage)}
} as iTopbarButton,
m("i.material-icons", "delete_forever")
),
m(TopbarButton,
{
onclick: () => {
let apps: iApplication[] = []
localDataModel.appnodes.forEach((appnode: Treenode) => {
if (appnode.isChecked) {
apps.push(appnode.nodevalue)
}
})
if (apps.length != 1) {
m.mount(gridMountpoints.modalhook, WrongApplicationEditSelectionMessage)
}
else {
m.mount(gridMountpoints.modalhook, {
view: (vnode) => {
return m(ApplicationForm, {
application: apps[0] as iApplication
} as iApplicationForm)
}
})
}
}
} as iTopbarButton,
m("i.material-icons", "edit")
)
)
)
)
)
)
}
}
}
function sortRoleNodes(node1: iTreenode, node2: iTreenode) {
// es soll zuerst nach Level und dann nach rowid sortiert werden
let v1 = (node1.nodevalue['level'].toString().padStart(6,'0') + node1.nodevalue['roleid']).toLocaleLowerCase('de-de')
let v2 = (node2.nodevalue['level'].toString().padStart(6,'0') + node2.nodevalue['roleid']).toLocaleLowerCase('de-de')
if (v1 < v2) {
return -1
}
if (v1 > v2) {
return 1
}
return 0
}
function applyRoles() {
let checkedApps = localDataModel.appnodes.filter((appnode: iTreenode) => {
return appnode.isChecked
})
if (checkedApps.length == 0) {
m.mount(gridMountpoints.modalhook, ErrorMessage)
return
}
m.mount(gridMountpoints.modalhook, {view: () => {return m(ApplyRolesWarning, {applyroles: doit})}})
function doit() {
checkedApps.forEach((appnode: iTreenode) => {
localDataModel.rolenodes.forEach((rolenode: iTreenode) => {
let approle: iTreenode = appnode.children.find((child: iTreenode) => {
return child.nodevalue['roleid'] === rolenode.nodevalue['roleid']
})
if (approle != undefined) {
approle.isChecked = rolenode.isChecked
}
else if (rolenode.isChecked) {
let newapprole = new Treenode(rolenode.nodevalue, false, [])
newapprole.isChecked = rolenode.isChecked
appnode.children.push(newapprole)
}
})
})
localDataModel.updateApplicationRoles()
}
}
const ErrorMessage: m.Component = {
view: (vnode) => {
return (
m(ModalView,
{
title: "Fehler",
type: ModalType.ERROR,
width: '33%'
} as iModalView,
m("p", "Es wurden noch keine Ziel-Anwendungen markiert!"),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "OK"),
]
)
)
)
}
}
export interface iApplyRolesWaring {
applyroles: Function
}
const ApplyRolesWarning: m.Component<iApplyRolesWaring> = {
view: (vnode) => {
return (
m(ModalView,
{
title: "Hinweis",
type: ModalType.MESSAGE,
width: '33%'
} as iModalView,
m("p", "Wenn Rollen nicht ausgewählt sind bedeutet dies, dass die Rollen in den ausgewählten Anwendungen gelöscht werden!"),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "Abbruch"),
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null); vnode.attrs.applyroles()}}, "OK"),
]
)
)
)
}
}
const WrongRoleEditSelectionMessage: m.Component = {
view: () => {
return m(WrongEditSelectionMessage, {elementname: "Rolle"} as iWrongEditSelectionMessage)
}
}
const WrongApplicationEditSelectionMessage: m.Component = {
view: () => {
return m(WrongEditSelectionMessage, {elementname: "Anwendung"} as iWrongEditSelectionMessage)
}
}
const DeleteRolesMessage: m.ClosureComponent = (vnode) => {
let roles: iRole[]
return {
oninit: (vnode) => {
roles = []
localDataModel.rolenodes.forEach((rolenode: iTreenode) => {
if (rolenode.isChecked) {
roles.push(rolenode.nodevalue)
}
})
},
view: (vnode) => {
return (
m(ModalView,
{
title: "Rollen löschen",
type: ModalType.MESSAGE,
width: '33%'
} as iModalView,
m("p", "Sie wollen die folgenden Rollen löschen:"),
m("ul",
roles.map((role: iRole) => {
return m("li", role.roleid)
})
),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right",
{
style: {'margin-left': '4px'},
onclick: () => {
localDataModel.deleteRoles(roles);
m.mount(gridMountpoints.modalhook, null)
}}, "Löschen"),
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "Abbrechen"),
]
)
)
)
}
}
}
interface iRoleForm {
role: iRole
}
const RoleForm: m.ClosureComponent<iRoleForm> = () => {
let form: HTMLFormElement = null;
let currentrole: iRole
function reset (e) {
form.reset()
}
function unmount () {
m.mount(gridMountpoints.modalhook, null)
}
function submit (e) {
e.preventDefault()
let formdata: FormData = new FormData(e.target as HTMLFormElement)
for (let element of formdata) {
currentrole[element[0]] = element[1]
}
localDataModel.updateRole(currentrole)
unmount()
}
return {
view: (vnode) => {
currentrole = (vnode.attrs.role === undefined) ? {} as iRole : vnode.attrs.role
return m(ModalView,
{
title: "Rolle",
type: ModalType.MESSAGE,
width: 'max-content'
} as iModalView,
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: submit},
m("label", "ID (intern)"),
m("div.w3-padding-small", currentrole.id),
m("label", "Rollen-ID"),
m("input.w3-input.w3-border", {name: 'roleid', type: 'text', value: currentrole.roleid}),
m("label", "Name"),
m("input.w3-input.w3-border", {name: 'name', type: 'text', value: currentrole.name}),
m("label", "Level"),
m("input.w3-input.w3-border", {name: 'level', type: 'number', value: currentrole.level}),
m("label", "Beschreibung"),
m("input.w3-input.w3-border", {name: 'beschreibung', type: 'text', value: currentrole.beschreibung}),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Speichern"),
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, onclick: unmount, type: "button"}, "Abbrechen"),
m("button.w3-button.w3-theme.w3-right", {onclick: reset, type: "button"}, "Zurücksetzen")
]
)
)
)
}
}
}
interface iApplicationForm {
application: iApplication
}
const ApplicationForm: m.ClosureComponent<iApplicationForm> = () => {
let form: HTMLFormElement = null;
let currentapp: iApplication
let isDirty = false
function reset (e) {
form.reset()
}
function unmount () {
if (isDirty) {
alert("is Dirty")
}
m.mount(gridMountpoints.modalhook, null)
}
function submit (e) {
e.preventDefault()
let formdata: FormData = new FormData(e.target as HTMLFormElement)
for (let element of formdata) {
currentapp[element[0]] = element[1]
}
localDataModel.updateApplication(currentapp)
unmount()
}
return {
view: (vnode) => {
currentapp = (vnode.attrs.application === undefined) ? {} as iApplication : vnode.attrs.application
return m(ModalView,
{
title: "Anwendung",
type: ModalType.MESSAGE,
width: 'max-content'
} as iModalView,
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: submit},
m("label", "ID (intern)"),
m("div.w3-padding-small", currentapp.id),
m("label", "Application-ID"),
m("input.w3-input.w3-border", {name: 'appid', type: 'text', value: currentapp.appid}),
m("label", "Name"),
m("input.w3-input.w3-border", {name: 'name', type: 'text', value: currentapp.name}),
m("label", "Minimum-Version"),
m("input.w3-input.w3-border", {name: 'minversion', type: 'text', value: currentapp.minversion}),
m("label", "Beschreibung"),
m("input.w3-input.w3-border", {name: 'beschreibung', type: 'text', value: currentapp.beschreibung}),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Speichern"),
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, onclick: unmount, type: "button"}, "Abbrechen"),
m("button.w3-button.w3-theme.w3-right", {onclick: reset, type: "button"}, "Zurücksetzen")
]
)
)
)
}
}
}
const DeleteApplicatonsMessage: m.ClosureComponent = (vnode) => {
let apps: iApplication[]
return {
oninit: (vnode) => {
apps = []
localDataModel.appnodes.forEach((appnode: iTreenode) => {
if (appnode.isChecked) {
apps.push(appnode.nodevalue)
}
})
},
view: (vnode) => {
return (
m(ModalView,
{
title: "Anwendungen löschen",
type: ModalType.MESSAGE,
width: '33%'
} as iModalView,
m("p.w3-panel.w3-red",
[
"Vor dem Löschen muss sichergestellt sein, dass es noch mindestens eine App gibt, in der die Rolle 'SysAdmin' ",
"vergeben ist und in der ein Benutzer diese Rolle besitzt!"
]
),
m("p", "Sie wollen die folgenden Anwendungen löschen:"),
m("ul",
apps.map((app: iApplication) => {
return m("li", app.appid)
})
),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right",
{
style: {'margin-left': '4px'},
onclick: () => {
localDataModel.deleteApplications(apps);
m.mount(gridMountpoints.modalhook, null)
}}, "Löschen"),
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "Abbrechen"),
]
)
)
)
}
}
}
@@ -0,0 +1,40 @@
import m from "mithril"
import { get_menustack, menubar } from "../header"
import { userlist } from "./userpage"
import { applicationpage } from "./applicationpage"
let mountpoint = null
function close_page() {
get_menustack().pop()
m.route.set("/blankpage")
}
function show_userlist() {
m.mount(mountpoint, userlist)
}
function show_applist() {
m.mount(mountpoint, applicationpage)
}
export const appmanagementpage: m.Component = {
oncreate: (vnode) => {
mountpoint = vnode.dom
},
oninit: (vnode) => {
get_menustack().push(menu)
},
view: (vnode) => {
return m("div", m(userlist))
}
}
const menu: m.Component = {
view: (vnode) => {
return m(menubar, [
m("a.w3-button", {onclick: show_userlist}, "Benutzer"),
sessionStorage.getItem("CurrentUserIsSysAdmin") != null ?
m("div.w3-button", {onclick: show_applist}, "Anwendungen"):null,
m("div.w3-button.w3-right", {onclick: close_page}, "Beenden"),
])
}
}
+76
View File
@@ -0,0 +1,76 @@
import m from "mithril"
import { gridMountpoints } from "../mainlayout"
import {ModalType, ModalView, iModalView} from "../widgets/Modal"
export interface iWrongEditSelectionMessage{
elementname: string
}
export const WrongEditSelectionMessage: m.Component<iWrongEditSelectionMessage> = {
view: (vnode) => {
return (
m(ModalView,
{
title: "Falsche Auswahl",
type: ModalType.ERROR,
width: '33%'
} as iModalView,
m("p", `Zum Editieren muss genau 1 ${vnode.attrs.elementname} ausgewählt sein!`),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "OK"),
]
)
)
)
}
}
export interface iServerErrorMessage {
error: {
messages: string[],
status: number
}
}
export const ServerErrorMessage: m.Component<iServerErrorMessage> = {
view: (vnode) => {
return (
m(ModalView,
{
title: "Fehler",
type: ModalType.ERROR,
width: "33%"
} as iModalView,
m("div.w3-panel",
vnode.attrs.error.messages.map((message) => {
return m("div", message)
}),
m("div.w3-small", `(HTML-Status: ${vnode.attrs.error.status})`)
),
m("div.w3-bar.w3-padding-16",
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "OK"),
)
)
)
}
}
export function showServerError (e) {
let servererror:iServerErrorMessage = {error: {messages:[], status: e.code}}
console.log(JSON.stringify(e), e.message)
if (e.response != undefined && e.response["messages"] != undefined) {
if (!Array.isArray(e.response["messages"])) {
e.response["messages"] = [e.response["messages"]]
}
servererror.error.messages = e.response.messages
}
else if (e.response != undefined) {
servererror.error.messages = [JSON.stringify(e.response)]
}
else {
servererror.error.messages = [JSON.stringify(e)]
}
m.mount(gridMountpoints.modalhook, {
view: () => {return m(ServerErrorMessage, {error: servererror.error } as iServerErrorMessage)}
})
}
+117
View File
@@ -0,0 +1,117 @@
import m from "mithril";
import {ModalView, iModalView} from "../widgets/Modal"
import {iUser} from "./models/users"
import {showServerError} from "./authmessages"
import { headerdata } from "../header";
export interface iLoginscreen {
setnextpage: string
}
export const loginscreen: m.ClosureComponent<iLoginscreen> = (vnode) => {
let form: HTMLFormElement = null;
function reset (e) {
form.reset()
}
function checklogin(e: Event) {
e.preventDefault()
let formdata: FormData = new FormData(e.target as HTMLFormElement)
let user: iUser
m.request ({
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
url: "/auth/checklogin",
body: Object.fromEntries(formdata.entries())
})
.then((result) => {
user = result as iUser;
// global.appglobals.currentuser = user.username
// global.appglobals.currentuserid = user.userid
sessionStorage.setItem("CurrentUser", user.username);
headerdata.username = user.username
headerdata.userid = user.userid
// nur wenn es sich um einen SysAdmin handelt, wird der SessionStorage gesetzt. Will man später wissen,
// ob der aktuelle User ein SysAdmin ist, braucht man nur auf das Vorhandensein des Schlüssels abzufragen
// und nicht auch noch auf den Inhalt
if (user.isSysAdmin) {
sessionStorage.setItem("CurrentUserIsSysAdmin", "1");
}
})
.then(() => {m.route.set(vnode.attrs.setnextpage)})
.catch((error) => {
sessionStorage.removeItem("CurrentUser");
sessionStorage.removeItem("CurrentUserIsSysAdmin");
showServerError(error)
})
}
return {
oninit: (vnode) => {
m.request({
method: "GET",
url: "/auth/logout"
})
.then (() => {
sessionStorage.removeItem("CurrentUser");
sessionStorage.removeItem("CurrentUserIsSysAdmin");
// global.appglobals.currentuser = "nicht angemeldet"
// global.appglobals.currentuserid = ""
})
},
view: (vnode) => {
return m(ModalView, {title: "Login", width: '33%'} as iModalView,
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: checklogin},
m("label", "Benutzer-ID"),
m("input.w3-input.w3-border", {name: 'userid', type: 'text'}),
m("label", "Kennwort"),
m("input.w3-input.w3-border", {name: 'password', type: 'password'}),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Login"),
m("button.w3-button.w3-theme.w3-right", {onclick: reset, type: "button"}, "Zurücksetzen"),
]
)
)
)
}
}
}
export const changepasswordscreen: m.ClosureComponent = () => {
let form: HTMLFormElement = null;
function reset (e) {
form.reset()
}
function changepassword(e) {
e.preventDefault()
let formdata: FormData = new FormData(e.target as HTMLFormElement)
m.request ({
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
url: "/auth/changepassword",
body: Object.fromEntries(formdata.entries())
})
.then(() => {m.route.set("/login")})
.catch((e) => {showServerError(e)})
}
return {
view: (vnode) => {
return m(ModalView, {title: "Kennwort ändern", width: '33%'} as iModalView,
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: changepassword},
m("label", "Benutzer-ID"),
m("input.w3-input.w3-border", {name: 'Userid', type: 'text', value: headerdata.userid, readonly: true}),
m("label", "Altes Kennwort"),
m("input.w3-input.w3-border", {name: 'OldPassword', type: 'password'}),
m("label", "Neues Kennwort"),
m("input.w3-input.w3-border", {name: 'NewPassword1', type: 'password'}),
m("label", "Neues Kennwort"),
m("input.w3-input.w3-border", {name: 'NewPassword2', type: 'password'}),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Kennwort ändern"),
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, onclick: reset, type: "button"}, "Zurücksetzen"),
m("button.w3-button.w3-theme.w3-right", {onclick: () => {history.back()}, type: "button"}, "Abbrechen"),
]
)
)
)
}
}
}
@@ -0,0 +1,18 @@
import {iRole} from "./roles"
export interface iApplication {
id: number
appid: string
name: string
minversion: string
beschreibung: string
roles: iRole[]
}
export interface iUserApp {
id: number
userid: number
appid: number
userapp_roles : iRole[]
}
+8
View File
@@ -0,0 +1,8 @@
export interface iRole {
id: number
roleid: string
name: string
level: number
beschreibung: string
}
+24
View File
@@ -0,0 +1,24 @@
import { iUserApp } from "./applications"
import { iRole } from "./roles"
export interface iUser {
id: number
userid: string
username: string
password: string
email: string
telefon: string
isSysAdmin: boolean
userapps: iUserApp[]
}
export function hasAppRole(user: iUser, appid: number, roleid: number) {
let userapp: iUserApp = user.userapps.find((userapp: iUserApp) => {return userapp.appid == appid})
if (userapp != undefined) {
let userapprole: iRole = userapp.userapp_roles.find((role: iRole) => {return role.id == roleid})
if (userapprole != undefined) {
return true
}
}
return false
}
+364
View File
@@ -0,0 +1,364 @@
import m from "mithril"
import {hasAppRole, iUser} from "./models/users"
import {iApplication} from "./models/applications"
import {iRole} from "./models/roles"
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "../widgets/Treetable_V2"
import {gridMountpoints, maingrid} from "../mainlayout"
import {ModalView, ModalType, iModalView} from "../widgets/Modal"
import { iWrongEditSelectionMessage, WrongEditSelectionMessage, showServerError} from "./authmessages"
interface iTabelrow {
rowtype: any
col1: any
cols: any[]
info?: {
parentuserid: number
parentappid: number
roleid: number
ischecked: boolean
}
}
const localDataModel = {
userlist: [] as iUser[],
applist: [] as iApplication[],
usernodes: [] as iTreenode[],
startexpanded: false,
loadData: () => {
Promise.all([
m.request({
method: "GET",
url: "/auth/getusers"
}).then((result) => localDataModel.userlist = result as iUser[]),
m.request({
method: "GET",
url: "/auth/getapplications"
}).then((result) => localDataModel.applist = result as iApplication[]),
])
.then(() => {
localDataModel.usernodes = []
localDataModel.userlist.map((u) => {
let userrow: iTabelrow = {
rowtype: 'user',
col1: u.userid,
cols: [u.username, u.email, u.telefon]
}
let usernode: iTreenode = new Treenode(userrow, localDataModel.startexpanded, [])
localDataModel.applist.forEach((app: iApplication) => {
let approw: iTabelrow = {
rowtype: "app",
col1: app.appid,
cols: [app.name, app.minversion],
info: {
ischecked: false,
parentuserid: u.id,
parentappid: null,
roleid: null
}
}
let appnode: Treenode = new Treenode(approw, localDataModel.startexpanded, [])
app.roles.forEach((role: iRole) => {
let ischecked = hasAppRole(u, app.id, role.id)
let rolerow: iTabelrow = {
rowtype: "role",
col1: role.name,
cols: [role.roleid],
info: {
parentuserid: u.id,
parentappid: app.id,
ischecked: ischecked,
roleid: role.id
}
}
let rolenode: iTreenode = new Treenode(rolerow, localDataModel.startexpanded, [])
appnode.children.push(rolenode)
})
usernode.children.push(appnode)
})
localDataModel.usernodes.push(usernode)
})
})
.catch((e) => {
if (e.code == 401) {
m.route.set("/login")
}
})
},
updateUser: (user: iUser) => {
m.request ({
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
url: "/auth/updateuser",
body: user
})
.catch((e) => {showServerError(e)})
.finally(() => localDataModel.loadData())
},
deleteUsers: (users: iUser[]) => {
m.request({
method: "POST",
url: "/auth/deleteusers",
body: users.map((u: iUser) => {return u.userid})
})
.catch((e) => {showServerError(e)})
.finally(() => localDataModel.loadData())
},
applyUserAppRole: (tablerow: iTabelrow) => {
m.request ({
method: "POST",
url: "/auth/applyuserapprole",
body: tablerow.info
})
.catch((e) => {showServerError(e); localDataModel.loadData()})
}
}
export const userlist: m.ClosureComponent = (vnode) => {
return {
oninit: (vnode) => {
localDataModel.loadData()
},
view: (vnode) => {
return m("div.w3-container.w3-content", {style: {'max-width': 'min-content', display: 'flex', 'flex-direction': 'column', height: 'inherit', 'overflow-y': 'inherit'}},
m("h1", "Benutzerliste"),
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
m(Treetable,
{
startexpanded: localDataModel.startexpanded,
treenodes: localDataModel.usernodes,
columnmapper: (node: iTreenode) => {
let row: iTabelrow = node.nodevalue
let isChecked = hasCheckedChilds(node)
if (row.rowtype == "user") {
return [
m("strong", row.col1),
m("strong", row.cols[0]),
m(isChecked?"div.w3-text-green":"div.w3-text-red", {style: {display: 'flex'}},
// m("div", {style: {padding: '0px', height:'24px', width: '24px'}},
// m("i.material-icons", isChecked?null:"dangerous"),
// ),
m("div", isChecked?"aktiv":"inaktiv")
),
m("div.w3-small",
m("div", row.cols[1]),
m("div", row.cols[2])
),
m(EditUserButton, {userid: row.col1} as iEditUserButton),
m(DeleteUserButton, {userid: row.col1} as iDeleteUserButton)
]
}
else if (row.rowtype == "role") {
return [
m("span.w3-small", row.col1),
m("span.w3-button", {
style: {padding: '0px'},
onclick: () => {
row.info.ischecked = !row.info.ischecked
localDataModel.applyUserAppRole(row)
}
},
m("i.material-icons.w3-theme-light", row.info.ischecked?"check_box":"check_box_outline_blank")
),
m("span.w3-small", row.cols[0]),
m("span.w3-small", row.cols[1]),
m("span", ""),
m("span", "")
]
}
else {
// rowtype "app"
let isChecked = hasCheckedChilds(node)
return [
m(isChecked?"span":"strong.w3-text-red", row.col1),
m("div", {style: {padding: '0px', height:'24px', width: '24px'}},
m("i.material-icons", isChecked?"check":null)
),
m("span", row.cols[0]),
m("span", row.cols[1]),
m("span", ""),
m("span", "")
]
}
}
} as iTreetable,
m(TopbarButton, {
onclick: () => {
m.mount(gridMountpoints.modalhook, {
view: (vnode) => {
return m(UserForm, {user: {id: -1} as iUser})
}
})
}} as iTopbarButton,
m("i.material-icons", "add")
)
)
)
)
}
}
}
function hasCheckedChilds(parenttreenode: iTreenode): boolean {
return parenttreenode.children.some((treenode: iTreenode) => {
if (treenode.hasChildren) {
return hasCheckedChilds(treenode)
}
let tablerow = treenode.nodevalue as iTabelrow
return tablerow.info.ischecked
})
}
interface iUserForm {
user: iUser
}
const UserForm: m.ClosureComponent<iUserForm> = () => {
let form: HTMLFormElement = null;
let currentuser: iUser
function reset (e) {
form.reset()
}
function unmount () {
m.mount(gridMountpoints.modalhook, null)
}
function submit (e) {
e.preventDefault()
let formdata: FormData = new FormData(e.target as HTMLFormElement)
for (let element of formdata) {
currentuser[element[0]] = element[1]
}
localDataModel.updateUser(currentuser)
unmount()
}
return {
oninit: (vnode) => {
currentuser = (vnode.attrs.user === undefined) ? {} as iUser : vnode.attrs.user
},
view: (vnode) => {
return m(ModalView,
{
title: "Benutzer",
type: ModalType.MESSAGE,
width: 'max-content'
} as iModalView,
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: submit},
m("label", "ID (intern)"),
m("div.w3-padding-small", currentuser.id),
m("label", "Benutzer-ID"),
m("input.w3-input.w3-border", {name: 'userid', type: 'text', value: currentuser.userid}),
m("label", "Name"),
m("input.w3-input.w3-border", {name: 'username', type: 'text', value: currentuser.username}),
m("label", "E-Mail"),
m("input.w3-input.w3-border", {name: 'email', type: 'email', value: currentuser.email}),
m("label", "Telefon"),
m("input.w3-input.w3-border", {name: 'telefon', type: 'tel', value: currentuser.telefon}),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Speichern"),
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, onclick: unmount, type: "button"}, "Abbrechen"),
m("button.w3-button.w3-theme.w3-right", {onclick: reset, type: "button"}, "Zurücksetzen")
]
)
)
)
}
}
}
const WrongUserEditSelectionMessage: m.Component = {
view: () => {
return m(WrongEditSelectionMessage, {elementname: "Benutzer"} as iWrongEditSelectionMessage)
}
}
interface iDeleteUsersMessage {
userid: string
}
const DeleteUsersMessage: m.ClosureComponent<iDeleteUsersMessage> = (vnode) => {
let users: iUser[]
return {
oninit: (vnode) => {
users = localDataModel.userlist.filter((u: iUser) => {
return u.userid == vnode.attrs.userid
})
},
view: (vnode) => {
return (
m(ModalView,
{
title: "Benutzer löschen",
type: ModalType.MESSAGE,
width: '33%'
} as iModalView,
m("p", "Sie wollen den folgenden Benutzer löschen:"),
m("ul",
users.map((user: iUser) => {
return m("li", `${user.userid} (${user.username})`)
})
),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right",
{
style: {'margin-left': '4px'},
onclick: () => {
localDataModel.deleteUsers(users);
m.mount(gridMountpoints.modalhook, null)
}}, "Löschen"),
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "Abbrechen"),
]
)
)
)
}
}
}
interface iDeleteUserButton {
userid: string
}
export const DeleteUserButton: m.ClosureComponent<iDeleteUserButton> = () => {
return {
view: (vnode) => {
return m("button.w3-button.w3-bar-item", {
style: {'min-width': '24px', height: '24px', padding: '0'},
onclick: () => {
m.mount(gridMountpoints.modalhook, {view: () => {
return m(DeleteUsersMessage, {userid: vnode.attrs.userid} as iDeleteUsersMessage)
}})
},
title: "Benutzer löschen"
},
m("i.material-icons", "delete_forever")
)
}
}
}
interface iEditUserButton {
userid: string
}
export const EditUserButton: m.ClosureComponent<iEditUserButton> = () => {
return {
view: (vnode) => {
return m("button.w3-button.w3-bar-item", {
style: {'min-width': '24px', height: '24px', padding: '0'},
onclick: () => {
let users: iUser[] = localDataModel.userlist.filter((u:iUser) => {
return u.userid == vnode.attrs.userid
})
m.mount(gridMountpoints.modalhook, {view: () => {
return m(UserForm, {user: users[0]} as iUserForm)
}})
},
title: "Benutzer bearbeiten"
},
m("i.material-icons", "edit")
)
}
}
}
+74
View File
@@ -0,0 +1,74 @@
import m from "mithril"
export var headerdata = {
date: new Date(),
version: "",
username: "Nicht angemeldet",
userid: "Nicht angemeldet",
appheader: "Der gläserne Kunde"
}
export const header: m.Component = {
view: (vnode) => {
return m("div.w3-row-padding.w3-theme",
m("div.w3-twothird.w3-padding-16", {style: "display: flex; justify-content: space-between;"},
// <img src="pfad/zum/bild.jpg" width="300" height="200" alt="Beschreibung des Bildes">
m("img", {src: "static/KBplus-Logo.png", height: 80, alt: "KBPlus CRM Logo"}),
m("h1",
m("div.w3-cell", headerdata.appheader)
)
),
m("div.w3-third.w3-display-container.w3-small",
m("table.w3-right",
m("tr.w3-hide-small",
m("td", "Datum:"),
m("td", headerdata.date.toLocaleDateString())
),
m("tr.w3-hide-small",
m("td", "Version:"),
m("td", headerdata.version)
),
m("tr",
m("td", "Benutzer:"),
m("td", headerdata.username)
)
)
)
)
}
}
export const menubar: m.Component = {
view: (vnode) => {
return m("div.w3-bar.w3-black",
vnode.children
)
}
}
let menustack: MenuStack = null
export function init_menustack(menubarmountpoint):void {
if (menustack == null) {menustack = new MenuStack(menubarmountpoint)}
}
export function get_menustack(): MenuStack {
return menustack
}
class MenuStack {
private menustack = []
private menubarmountpoint = null
constructor(menubarmountpoint) {
this.menustack = []
this.menubarmountpoint = menubarmountpoint
}
push(menubar) {
this.menustack.push(menubar)
m.mount(this.menubarmountpoint, menubar)
}
pop() {
this.menustack.pop()
let menubar = this.menustack[this.menustack.length - 1]
m.mount(this.menubarmountpoint, menubar)
}
}
+18
View File
@@ -0,0 +1,18 @@
import m from "mithril"
import {Form, iForm} from "./widgets/form"
import { Input, iInput} from "./widgets/inputs"
export const kunde: m.Component = {
view: (vnode) => {
return m("div.w3-container.w3-twothird",
m("h1", "Kundenstammblatt"),
m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [
m("label", "Kundenname (ganz neu)", m(Input, {name: "kundenname", value: "Musterfirma GmbH", type: "text"} as iInput)),
m("label", "Straße", m(Input, {name: "strasse", value: "Musterstraße 1", type: "text"} as iInput)) ,
m("label", "PLZ", m(Input, {name: "plz", value: "12345", type: "text"} as iInput)),
m("label", "Ort", m(Input, {name: "ort", value: "Musterstadt", type: "text"} as iInput)),
m("label", "Land", m(Input, {name: "land", value: "Deutschland", type: "text"} as iInput))
])
)
}
}
+50
View File
@@ -0,0 +1,50 @@
import m from "mithril";
export var gridMountpoints = {
header: null,
menubar: null,
main: null,
modalhook: null
}
export const maingrid: m.ClosureComponent = (vnode) => {
let gridstyle = {
'display': 'grid',
'grid-template-rows': 'auto auto 1fr',
'height': '100vh',
'width': '100vw'
}
return {
view: (vnode) => {
return [
m("div", {style: gridstyle},
[
m("div", {
oncreate: (vnode) => { gridMountpoints.header = vnode.dom },
style: {'grid-row': '1'}
}),
m("div", {
oncreate: (vnode) => { gridMountpoints.menubar = vnode.dom },
style: {'grid-row': '2'}
}),
m("div",
{
//oncreate: (vnode) => { gridMountpoints.main = vnode.dom },
style: {'grid-row': "3", 'overflow-y': 'auto', 'height': '100%'}
},
m("div",
{
oncreate: (vnode) => { gridMountpoints.main = vnode.dom },
style: {'overflow-y': 'auto', height: 'inherit'}
}
)
)
]
),
m("div#modalhook", {
oncreate: (vnode) => { gridMountpoints.modalhook = vnode.dom}
})
]
}
}
}
+18
View File
@@ -0,0 +1,18 @@
import m from "mithril"
import {Form, iForm} from "./widgets/form"
import { Input, iInput} from "./widgets/inputs"
export const Projekt: m.Component = {
view: (vnode) => {
return m("div.w3-container.w3-twothird",
m("h1", "Projekt-Stammblatt"),
m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [
m("label", "Kundenname", m(Input, {name: "kundenname", value: "Musterfirma GmbH", type: "text"} as iInput)),
m("label", "Projektname", m(Input, {name: "projektname", value: "Musterprojekt", type: "text"} as iInput)) ,
m("label", "Zweck", m(Input, {name: "zweck", value: "Musterzweck", type: "text"} as iInput)),
m("label", "Verantwortlicher", m(Input, {name: "verantwortlicher", value: "König Dickbauch", type: "text"} as iInput)),
m("label", "Startdatum", m(Input, {name: "startdatum", value: "2023-01-01", type: "date"} as iInput))
])
)
}
}
+71
View File
@@ -0,0 +1,71 @@
import m from "mithril"
import {BaseView} from "./base"
export enum ModalType {
'MESSAGE',
'ERROR'
}
export interface iModalView {
title: string
subtitle: string
topposition: string
width: string
type: ModalType
}
export class ModalView extends BaseView implements iModalView{
title:string
subtitle:string
topposition:string
width:string
type:ModalType
constructor (vnode) {
super()
this.title = super.getattr(vnode, "title", "Modal")
this.subtitle = super.getattr(vnode, "subtitle", "")
this.topposition = super.getattr(vnode, "topposition", "10%")
this.width = super.getattr(vnode, "width", 'max-content')
this.type = super.getattr(vnode, 'type', ModalType.MESSAGE)
}
view (vnode) {
return m("div.w3-modal", {style: {display:'block'}},
m("div.w3-modal-content.w3-card-4", {style: {width: this.width, top: this.topposition}},
[
m(modalHeader, {type: this.type},
[
m(title, this.title),
(this.subtitle ? m(subtitle, this.subtitle) : null)
]
),
m(modalContent, vnode.children)
]
)
)
}
}
const title = {
view: (vnode) => {
return m("h1", vnode.children)
}
}
const subtitle = {
view: (vnode) => {
return m("h3", vnode.children)
}
}
const modalHeader = {
view: (vnode) => {
let color = ".w3-theme"
if (vnode.attrs["type"] == ModalType.ERROR) {
color = ".w3-red"
}
return m("header.w3-container" + color, vnode.children)
}
}
const modalContent = {
view: (vnode) => {
return m("div.w3-container.w3-theme-l1", vnode.children)
}
}
+231
View File
@@ -0,0 +1,231 @@
import m from "mithril"
export interface iTreenode {
nodevalue: any
children: iTreenode[]
expand: boolean
hasChildren: boolean
isChecked?: boolean
}
export class Treenode implements iTreenode {
nodevalue:any = null
children: iTreenode[] = []
expand: boolean = true
isChecked?: boolean = false
constructor(nodevalue: any = null, expand: boolean = false, children: iTreenode[] = [], isChecked: boolean = false) {
this.nodevalue = nodevalue
this.children = children
this.expand = expand
this.isChecked = isChecked
}
get hasChildren() {
return (this.children.length > 0)
}
}
export interface Columnmapper {
(row:iTreenode) : any[]
}
export interface iTreetable {
treenodes: iTreenode[]
columnmapper: Columnmapper
onRowMarked?: (node: iTreenode) => void
filter?: (row:iTreenode, searchvalue:string) => boolean
}
export const Treetable: m.ClosureComponent<iTreetable> = (vnode) => {
let onRowMarked = vnode.attrs.onRowMarked === undefined ? (node: iTreenode) => {} : vnode.attrs.onRowMarked
let expandtoggle: boolean = true
let searchvalue:string = ""
function setSearchvalue(v:string) {
searchvalue = v
}
return {
view: (vnode) => {
return m("div.w3-border-theme", {style: {height: '99%', overflow: 'hidden', 'border-style': 'solid', 'border-width': '1px'}},
m("div.w3-bar.w3-theme",
m(TopbarButton, {
onclick: () => {
expandtoggle = true; changeTreenodes(vnode.attrs.treenodes, expandtoggle)
}},
m("i.material-icons", "unfold_more")
),
m(TopbarButton, {
onclick: () => {
expandtoggle = false; changeTreenodes(vnode.attrs.treenodes, expandtoggle)
}},
m("i.material-icons", "unfold_less")
),
vnode.children,
(vnode.attrs.filter != undefined) ? m(SearchField, {setSearchvalue: setSearchvalue} as iSearchfield) : null
),
m("div", {style: {height: '100%'}},
m("div", {style: {height: 'calc(100% - 32px', overflow: 'auto', width: '100%'}},
m("table.w3-table", {style: {'overflow-y': 'auto'}},
vnode.attrs.treenodes.map((tn:iTreenode, z) => {
if (vnode.attrs.filter == undefined || (vnode.attrs.filter != undefined && vnode.attrs.filter(tn, searchvalue) == true)) {
return m(Treerow, {
// ACHTUNG: Änderungen hier müssen unten beim rekursiven Aufruf der Treerow-Funktion ebenfalls eingebaut werden
treenode: tn,
level: 0,
columnmapper1: vnode.attrs.columnmapper,
onRowMarked: onRowMarked,
isEven: ((z % 2) == 0)} as iTreerow)
}
else {
return null
}
})
)
)
)
)
}
}
}
interface iTreerow {
treenode: iTreenode
level: number
columnmapper1: Columnmapper
onRowMarked: (node: iTreenode) => void
isEven?: boolean
}
let Treerow: m.ClosureComponent<iTreerow> = (vnode) => {
return {
view: (vnode) => {
return [
m("tr.w3-border-theme", {onmouseup: () => vnode.attrs.onRowMarked(vnode.attrs.treenode),
onkeyup: () => vnode.attrs.onRowMarked(vnode.attrs.treenode),
class: (vnode.attrs.isEven ? "w3-tablerow-even" : "w3-tablerow-odd"),
style: {'border-top-style': 'solid', 'border-top-width': '1px'}
},
m("td", // Zunächst wird ein td-Element für die erste Spalte der Tabelle erzeugt, damit dort die
// Einrückung in Abhängigkeit vom evtl. Vorhandensein von Kind-Elementen erledigt wird
m("div", {style: {width: 'max-content', 'margin-left': `${(vnode.attrs.level * 25)}px`}} ,
vnode.attrs.treenode.hasChildren
// Wenn der Treenode Kinder hat, dann soll der "expand_more/expand_less"-Button
// angezeigt werden
? m("span.w3-button", {
onclick: () => {changeTreenode(vnode.attrs.treenode, !vnode.attrs.treenode.expand)},
style: {padding: '0px',
'margin-right': '8px',
width: '24px',
height: '24px'}},
m("i.material-icons", vnode.attrs.treenode.expand?"expand_more":"navigate_next")
)
// Wenn der Trennode keine Kinder hat, dann soll kein "expand_more/expand_less"-Button
// angezeigt werden, allerdings soll durch 'margin-left' der entsprechende Platz des
// Buttons eingerückt werden, damit die Einrückungs-Level auch bei "kinderlosen" Treenodes
// denen der Trennodes mit Kindern entsprechen
: vnode.attrs.level == 0
? null // Wenn der kinderlos Treenode allerdings sirekt auf Level 0 ist, soll nocht
// eingerückt werden, da es bei Level 0 keine Eltern-Nodes mit Kindern und
// demzufolge mit "expand_more/expand_less"-Button gibt.
: m(
"span", {
style: {'margin-left': '24px',
'margin-right': '8px'}
}
),
// Nachdem die einrückung auf Grund der "hasChildren"-Abfrage erledigt ist, wird der Inhalt
// der ersten Spalte des Column-Mappers angezeigt
vnode.attrs.columnmapper1(vnode.attrs.treenode).splice(0,1)
)
),
// Dann werden die übrigen Spalten ganz normal als td-Elemente eingefügt
vnode.attrs.columnmapper1(vnode.attrs.treenode).splice(1).map((column) => {
return m("td", column)
})
),
// Falls der Trennode Kinder hat und expandiert werden soll, wird rekursiv die Trennode-Komponente für
// die Child-Nodes aufgerufen, ansonsten wir null zurückgegeben und die Einrückung hört auf
vnode.attrs.treenode.hasChildren && vnode.attrs.treenode.expand
? vnode.attrs.treenode.children.map((tn: iTreenode, z) => {
return m(Treerow, {
treenode: tn,
level: vnode.attrs.level + 1,
columnmapper1: vnode.attrs.columnmapper1,
onRowMarked: vnode.attrs.onRowMarked,
isEven: ((z % 2) == 0)
} as iTreerow)
})
: null
]
}
}
}
export interface iTopbarButton {
onclick: Function
tooltip?: string
}
export const TopbarButton: m.ClosureComponent<iTopbarButton> = () => {
return {
view: (vnode) => {
return m("button.w3-button.w3-bar-item", {
style: {'min-width': '32px', height: '32px', padding: '4px'},
onclick: vnode.attrs.onclick,
title: vnode.attrs.tooltip?vnode.attrs.tooltip:null
},
vnode.children
)
}
}
}
/** Die Funktion kann aufgerufen werden, wenn eine Treenode-Struktur hinsichtlich
* "Expand more/less" geändert werden soll.
*/
export function changeTreenodes(treenodes: Treenode[], expand: boolean) {
treenodes.forEach((treenode: Treenode) => {
changeTreenode(treenode, expand)
if (treenode.hasChildren) {
changeTreenodes(treenode.children, expand)
}
})
}
function changeTreenode(treenode: Treenode, expand: boolean) {
treenode.expand = expand
}
interface iSearchfield {
setSearchvalue: (v:string) => void
}
const SearchField:m.ClosureComponent<iSearchfield> = (vnode) => {
let inputfield: HTMLInputElement|null = null
function oninput(e:Event) {
if (inputfield) { // ✅ Null-Check
let v:string = inputfield.value
vnode.attrs.setSearchvalue(v)
}
}
return {
view: (vnode) => {
return m("div", {style: {display: "flex", border: "1px solid black", margin: '1px'}},
m("input.ggg-inputfocus", {
oncreate: (vnode) => {
inputfield = vnode.dom as HTMLInputElement
inputfield.focus()
},
type:"text",
style: {flex: '1', border: 'none'}, oninput: oninput
}),
m("button.w3-button", {
style: {'min-width': '28px', height: '28px', padding: '4px', border: 'none'},
onclick: () => {
if (inputfield) { // ✅ Null-Check
inputfield.value = ""
vnode.attrs.setSearchvalue("")
inputfield.focus()
}
}},
m("i.material-icons", {style: {'font-size': 'medium'}}, "clear")
)
)
}
}
}
+7
View File
@@ -0,0 +1,7 @@
export class BaseView {
getattr(vnode: any, attrname: string, defaultvalue: any) {
return vnode.attrs[attrname] === undefined ? defaultvalue : vnode.attrs[attrname]
}
}
+112
View File
@@ -0,0 +1,112 @@
import m from "mithril"
export interface iForm {
onchanged?: (e:Event) => void
onsubmit: (form:HTMLFormElement) => boolean
oncancel: (e:Event) => void
showbuttons?: boolean
/** Die Funktion gibt das HTMLFormElement zurück, welches in der Form-Componente angelegt wird */
onformcreate?: (form:HTMLFormElement) => void
}
export const Form:m.ClosureComponent<iForm> = (vnode) => {
let form: HTMLFormElement
let isDirty: boolean = false
let showButtons: boolean = vnode.attrs.showbuttons == undefined ? true : vnode.attrs.showbuttons
function canCloseForm(typ: string = "") {
if (!isDirty) {
return true
}
let text = (typ == "reset") ? "zurücksetzen" : "beenden"
return confirm (`Das Formular enthält noch ungespeicherte Eingaben. Wollen Sie das Formular trotzdem ${text}?`)
}
function onchanged (e:Event) {
isDirty = true
if (vnode.attrs.onchanged != undefined) {
vnode.attrs.onchanged(e)
}
}
function submit (e:Event) {
e.preventDefault()
if (vnode.attrs.onsubmit(form) == true) {
isDirty = false
}
}
function registerFormElement (f:HTMLFormElement) {
if (vnode.attrs.onformcreate != undefined) {
vnode.attrs.onformcreate(f)
}
}
function cancel(e:Event) {
e.preventDefault()
if (canCloseForm() == true) {
isDirty = false
if (vnode.attrs.oncancel != undefined) {
vnode.attrs.oncancel(e)
}
else {
history.back()
}
}
else {
e['redraw'] = false
}
}
function reset(e:Event) {
if (canCloseForm("reset")) {
isDirty = false
// Hier passiert nichts.
// Da mithril nach jedem User-Event (wie z.B. einem 'Click') ein redraw() ausführt, wird hier die Form
// neu gezeichnet und zwar mit den ursprünglichen Daten aus den zu grundeliegenden Datenmodel, das ja
// ohne submit noch nicht geändert wurde.
return
}
e['redraw'] = false
return
}
return {
onremove: (vnode) => {
let message = "remove"
if (isDirty) {
message += " isDirty"
}
console.log(message)
},
view: (vnode) => {
return m("form", {
onreset: () => {console.log("reset")},
onsubmit: submit,
oncreate: (vnode) => {
form = vnode.dom as HTMLFormElement
let elements = form.querySelectorAll("input,textarea")
for (let index = 0; index < elements.length; index++) {
const element = elements[index];
element.addEventListener("change", onchanged)
}
registerFormElement(form)
}
},
vnode.children,
showButtons == true ?
m("div.w3-bar.w3-padding-16",[
m("button.w3-button.w3-theme.w3-right.w3-mobile", {style: {margin: '0px 0px 2px 4px'}, type: "submit"}, "Speichern"),
m("button.w3-button.w3-theme.w3-right.w3-mobile", {style: {margin: '0px 0px 2px 4px'}, onclick: cancel, type: "button"}, "Abbrechen"),
m("button.w3-button.w3-theme.w3-right.w3-mobile", {onclick: reset, type: "button"}, "Zurücksetzen")
])
: null
)
}
}
}
export interface iFormBorder {
maxwidth: string
}
export const FormBorder: m.Component<iFormBorder> = {
view: (vnode) => {
let mw:string = vnode.attrs.maxwidth === undefined ? "600px" : vnode.attrs.maxwidth
return m("div.w3-border.w3-border-theme.w3-container.w3-content.w3-margin-top",
{style: {'max-width': mw, 'box-shadow': '3px 3px 3px darkgrey'}}, vnode.children)
}
}
+127
View File
@@ -0,0 +1,127 @@
import m from "mithril"
/**
* Der Vnode der Komponente 'Input' akzeptiert die folgenden Parameter:
* @property {string} name - Der Name des Feldes
* @property {any} value - der darzustellende Wert
* @property {string} type (optional) - der Type des Input Felds. Möglich sind alle Type des HTML-Elements 'Input'
* @property {string|number} width (optional) - die Breite des Input Felds. Möglich sind alle Längeneinheiten, die bom Width-Attribut möglich sind
* @property {number} min (optional) -
* @property {number} max (optional) -
* @property {number} step (optional) -
* @property {boolean} autofocus (optional) -
*/
export interface iInput {
name: string
value: any
type?: string
width?: string
min?: number
max?: number
step?: number
autofocus?: boolean
readonly?: boolean
required?:boolean
onchanged?: Function
}
/**
* Diese Komponente stellt ein Html-Input Element dar. Der darzustellende Wert wird einfach mit dem Attribut
* 'value' übergeben. Der eingegebene Wert muss dann später explizit aus Value abgefragt werden. Es findet also kein
* sutomatisches Binding statt.
*/
export const Input:m.Component<iInput> = {
onupdate: (vnode) => {
if (vnode.attrs.autofocus != undefined && vnode.attrs.autofocus == true) {
let e: HTMLElement = vnode.dom as HTMLElement
e.focus()
}
},
view: (vnode) => {
let attributes = {}
let width:number|string = '100%'
attributes['name'] = vnode.attrs.name
attributes['type'] = vnode.attrs.type == undefined ? 'text' : vnode.attrs.type
if (attributes['type'] == 'number') {
if (vnode.attrs.min != undefined) {
attributes['min'] = vnode.attrs.min
}
if (vnode.attrs.max != undefined) {
attributes['max'] = vnode.attrs.max
}
if (vnode.attrs.step != undefined) {
attributes['step'] = vnode.attrs.step
}
}
if (attributes['type'] == 'date') {
attributes['value'] = toDateInputValue(vnode.attrs.value)
}
else {
attributes['value'] = vnode.attrs.value
}
if (vnode.attrs.required != undefined && vnode.attrs.required == true) {
attributes['required'] = vnode.attrs.required
}
if (vnode.attrs.readonly != undefined && vnode.attrs.readonly == true) {
attributes['readonly'] = "true"
}
if (vnode.attrs.width != undefined) {
width = vnode.attrs.width
}
attributes['style'] = {'margin-bottom': '5px', 'width': width}
if (vnode.attrs.onchanged != undefined) {
attributes['onchange'] = vnode.attrs.onchanged
}
return m("input.w3-input.w3-border", attributes)
}
}
export interface iTextarea {
name: string
value: any
cols?: number
rows?: number
autofocus?: boolean
readonly?: boolean
}
/**
* Diese Komponente stellt ein Html-Textarea Element dar. Der darzustellende Wert wird einfach mit dem Attribut
* 'value' übergeben. Der eingegebene Wert muss dann später explizit aus Value abgefragt werden. Es findet also kein
* automatisches Binding statt.
*/
export const Textarea:m.Component<iTextarea> = {
onupdate: (vnode) => {
if (vnode.attrs.autofocus != undefined && vnode.attrs.autofocus == true) {
let e: HTMLElement = vnode.dom as HTMLElement
e.focus()
}
},
view: (vnode) => {
let attributes = {}
attributes['name'] = vnode.attrs.name
attributes['value'] = vnode.attrs.value
if (vnode.attrs.cols != undefined) {
attributes['cols'] = vnode.attrs.cols
}
if (vnode.attrs.rows != undefined) {
attributes['rows'] = vnode.attrs.rows
}
if (vnode.attrs.readonly != undefined && vnode.attrs.readonly == true) {
attributes['readonly'] = vnode.attrs.readonly
}
attributes['style'] = {'margin-bottom': '5px', "width": '100%'}
return m("textarea.w3-input.w3-border.w3-mobile", attributes)
}
}
export function toDateInputValue(date: Date): string {
console.log(date, typeof(date), (date instanceof Date))
let result: string = null
if (date instanceof Date && date != null) {
result = [date.getFullYear(), (date.getMonth() + 1).toString().padStart(2, "0"), date.getDate().toString().padStart(2, "0")].join("-")
}
return result
}
+39
View File
@@ -0,0 +1,39 @@
import m from "mithril"
interface iTabDef {
tabname: string
tabview: m.ComponentTypes
}
type TabDef = [string, m.ComponentTypes]
export interface iTabView {
tabviews: TabDef[]
}
export const TabView: m.ClosureComponent<iTabView> = (vnode) => {
let tv = {}
vnode.attrs.tabviews.forEach((item: TabDef) => {
tv[item[0]] = item[1]
})
let currTab = ""
function changeview(e) {
currTab = e.target.value
}
return {
view: (vnode) => {
return m("div", {style: {display: 'grid', 'grid-template-rows': "auto 1fr", height: '100%'}}, [
m("div.w3-bar.w3-theme.w3-border-bottom.w3-border-theme.w3-theme-action.w3-small",
Object.keys(tv).map((key) => {
if (currTab == "") {currTab = key}
let c = (key == currTab) ? ".w3-theme-light" : ""
return m("button.w3-bar-item.w3-button" + c, {value: key, onclick: changeview}, key)
})
),
m("div", m(tv[currTab]))
])
}
}
}
+149
View File
@@ -0,0 +1,149 @@
import m from "mithril"
import {gridMountpoints, maingrid} from "./Components/mainlayout"
import {header, menubar, init_menustack, get_menustack, headerdata} from "./Components/header"
import { appmanagementpage } from "./Components/auth/appmanagementpage"
import { changepasswordscreen, loginscreen } from "./Components/auth/login"
import {kunde} from "./Components/kunde"
import { Projekt } from "./Components/projekt"
function healthcheck () {
m.request({
method: "GET",
url: "/xhr/health-check"})
.then((result) => {
data.success = result['result']
data.time = result['time']
data.server = result['server']
data.projectname = result["projectname"]
data.clientappversion = result["clientappversion"]
})
return
}
function getClientappversion(): void {
//let version:string = document.querySelector('meta[name="clientappversion"]')['content']
const meta = document.querySelector<HTMLMetaElement>('meta[name="clientappversion"]');
const version = meta?.content ?? ''; // leerer String als Fallback
console.log(version)
headerdata.version = version
}
function check_currentuser (): boolean {
let result = true
const cu = sessionStorage.getItem("CurrentUser")
if (cu != null) {
headerdata.username = cu
}
else {
result = false
}
return result
}
let data = {
success: "n/a",
time: "n/a",
server: "n/a",
projectname: "n/a",
clientappversion: "n/a"
}
function logout() {
m.request({
method: "GET",
url: "/auth/logout"
})
.then((result) => {
console.log(result)
sessionStorage.removeItem("CurrentUser");
sessionStorage.removeItem("CurrentUserIsSysAdmin");
headerdata.username = "Nicht angemeldet"
if (result['logout'] == true) {m.route.set("/blankpage")}
})
.finally(() => {
location.href = "/index.html"
})
}
const hello: m.Component = {
oninit: (vnode) => {
healthcheck()
},
view: (vnode) => {
return m("div",
m("h1", "Health-Check Python App-Server"),
m(hello2),
m(knopf)
)
}
}
const hello2: m.Component = {
view: (vnode) => {
return m("ul",
m("li", "Result: " + data.success),
m("li", data.time),
m("li", data.server),
m("li", data.projectname),
m("li", data.clientappversion),
)
}
}
const knopf: m.Component = {
view: (vnode) => {
return m("button", {onclick: healthcheck}, "Aktualisieren")
}
}
const menu: m.Component = {
view: (vnode) => {
return m(menubar, [
m("a.w3-button.w3-bar-item", {href: "#!/kunde"}, "Kunden"),
m("a.w3-button.w3-bar-item", {href: "#!/projekt"}, "Projekte"),
m("a.w3-button.w3-bar-item", {href: "#!/routename"}, "Gesprächsnotizen"),
// m("a.w3-button.w3-bar-item", {onclick: healthcheck}, "Aktualisieren"),
m("div.w3-dropdown-hover",
m("button.w3-button", "Optionen"),
m("div.w3-dropdown-content.w3-bar-block.w3-card-4",
m("a.w3-button.w3-bar-item", {href: "#!/default"}, "Tags"),
m("a.w3-button.w3-bar-item", {href: "#!/default"}, "Datenexport"),
m("a.w3-button.w3-bar-item", {href: "#!/default"}, "Datenimport"),
)
),
m("div.w3-button.w3-right", {onclick: logout}, "Logout"),
m("div.w3-dropdown-hover.w3-right",
m("button.w3-button", "System"),
m("div.w3-dropdown-content.w3-bar-block.w3-card-4",
m("a.w3-button.w3-bar-item", {href: "#!/login"}, "Login"),
m("a.w3-button.w3-bar-item", {href: "#!/changepassword"}, "Kennwort ändern"),
m("a.w3-button.w3-bar-item", {href: "#!/usermanagement"}, "Verwalten"),
)
)
])
}
}
let mountpoint = document.getElementById("body")
m.mount(mountpoint, maingrid)
m.mount(gridMountpoints.header, header)
m.mount(gridMountpoints.main, hello)
init_menustack(gridMountpoints.menubar)
get_menustack().push(menu)
getClientappversion()
if (check_currentuser() == false) {
m.route.set("/login")
}
m.route(gridMountpoints.main, "/blankpage", {
"/blankpage": hello,
"/usermanagement": appmanagementpage,
"/changepassword": changepasswordscreen,
"/login": loginscreen,
"/kunde": kunde,
"/projekt": Projekt
})
+42
View File
@@ -0,0 +1,42 @@
import { readFileSync } from 'fs';
var result = ""
// https://stackoverflow.com/questions/55409195/reading-ini-file-using-javascript
function parseINIString(data){
var regex = {
section: /^\s*\[\s*([^\]]*)\s*\]\s*$/,
param: /^\s*([^=]+?)\s*=\s*(.*?)\s*$/,
comment: /^\s*;.*$/
};
var value = {};
var lines = data.split(/[\r\n]+/);
var section = null;
lines.forEach(function(line){
if(regex.comment.test(line)){
return;
}else if(regex.param.test(line)){
var match = line.match(regex.param);
if(section){
value[section][match[1]] = match[2];
}else{
value[match[1]] = match[2];
}
}else if(regex.section.test(line)){
var match = line.match(regex.section);
value[match[1]] = {};
section = match[1];
}else if(line.length == 0 && section){
section = null;
};
});
return value;
}
export function getIniValue(value){
return result["APP"][value]
}
var inifile = readFileSync("../App_Server/root/config.ini").toString()
result = parseINIString(inifile)
+1299
View File
File diff suppressed because it is too large. Load diff
+13
View File
@@ -0,0 +1,13 @@
{
"dependencies": {
"mithril": "^2.2.2"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@types/mithril": "^2.2.7",
"rollup": "^4.21.2",
"typescript": "^6.0.3"
}
}
+42
View File
@@ -0,0 +1,42 @@
// Die Config-Datei muss ab Rollup Version 3 die Endung .mjs haben. (s. Rollup Dokumentation "Migration to Rollup 3")
import {nodeResolve} from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';
// import json from '@rollup/plugin-json';
import {getIniValue} from "./inireader.mjs"
var version = getIniValue("version")
var projectname = getIniValue("projectname")
export default {
input: './tsout/index.js',
output: [
{
file: `/usr/opt/pythonapp/NGINX_Server/html/static/${projectname}_${version}.js`,
format: 'iife',
name: `${projectname}`
},
{
file: `/usr/opt/pythonapp/NGINX_Server/html/static/${projectname}_${version}.min.js`,
format: 'iife',
name: `${projectname}`,
plugins: [
terser()
]
}
],
plugins: [
nodeResolve({
// Der folgende Eintrag ist erforderlich, weil das tsout-Verzeichnis und somit das Input-Verzeichnis für Rollup,
// welches die Typescript-Kompilierten Programme enthält, nicht als Unterverzeichnis des Projekt-Verzeichnisses
// (...\UWSGI-Server\vassals\timesheet\client\src) angelegt ist, sondern parallel dazu.
// Ohne den folgenden Eintrag findet Rollup dann nicht das "Mithril"-Modul und es kommt zu Fehlern ("global nicht gefunden" usw.)
// Zusätzlich verwirrend war, dass der Fehler nicht auftrat, wenn das commonjs-Plugin mit der Version 21 eingesetzt war.
// Erst nachdem ich herausgefunden hatte, dass der nachfolgende Eintrag im Node-Resolve-Plugin den Fehler behob, konnte
// ich das commenjs-Plugin in der aktuellsten Version einsetzten.
modulePaths: ['node_modules'],
}),
commonjs()
// json()
]
};
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"allowJs": false,
"outDir": "./tsout",
"module": "ESNext",
"target": "ES2019",
"allowSyntheticDefaultImports": true,
"strict": false
}
}
+4
View File
@@ -0,0 +1,4 @@
ARG NGINX_VERSION
FROM nginx:${NGINX_VERSION}
RUN ln -sf /usr/share/zoneinfo/Europe/Berlin /etc/localtime
RUN mkdir /etc/certs
+22
View File
@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDmjCCAoKgAwIBAgIUIDBuHg1o+s/XylnF+ZGrpcD1PDIwDQYJKoZIhvcNAQEL
BQAwWzELMAkGA1UEBhMCREUxDDAKBgNVBAgMA05SVzERMA8GA1UEBwwIU2llZ2J1
cmcxFjAUBgNVBAoMDVNjaG9zY2hpc0NsdWIxEzARBgNVBAMMCm15bG9jYWwuZGUw
HhcNMjAwNTIxMTAzNDEzWhcNMzAwNTE5MTAzNDEzWjBdMQswCQYDVQQGEwJERTEM
MAoGA1UECAwDTlJXMREwDwYDVQQHDAhTaWVnYnVyZzEWMBQGA1UECgwNU2Nob3Nj
aGlzQ2x1YjEVMBMGA1UEAwwMKi5teWxvY2FsLmRlMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAoSgsxm7iNqNK6MVwXebTCUDCW3hxqd4bmG/HR9VxfIp8
eQDBbJNe/q+sOmEG2D6CNNPMuOlerqGJQnqzG83nNhEdVjMFdkgIEs2XjS+CgNjX
P442tYAibLxX/9UBQCYODA1RNv65dRNPxMCRQrMke2b6nU2+5EwBTFoxIQ5otarN
PPDRu19u5yqJQwxcC6zZ3sNmq/jOLkTy9g+JDrMRydtArV6Y6LUsZBTUPT5cKO+m
fQGe7D7b5+YBdu9TI4YpzG6/7Z3dlEehBJiWpLSdKD2FlN/jidiNr/uslpTIxdEV
mK7342LZuXOlwEdVSsP8lafnzV3WtH/nRZxZ7rpDNwIDAQABo1QwUjAfBgNVHSME
GDAWgBRyi8YbpjAj5TN2v2vRGfVcnM4kBjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE
8DAXBgNVHREEEDAOggwqLm15bG9jYWwuZGUwDQYJKoZIhvcNAQELBQADggEBAAdz
t0/rhsV+91BfkO1yUUm0m4H5vLsY4YLjoxhXqGRGwfp7CAV7n5iS4akMzVrs+y+4
Q8AbVeGsrv3AHWZ+ZKL64AGjFnTVXEeI1YN6cdwPkAvmnLi4QuXCRlJdDTEzJ6pf
4mHgHWNIT3H3GkGGLJLrBdz1NBn4QHt+7UHkBtYcWiGB9JOp0ungEdNmzA9fOPNo
eLtkKe1QuvlafUhKxGmnIiQyDZ+DI8EvDAX22qakcLyNeP5gxptDEf6nx2GUupVt
A+qxI+Rfg64KtiK6J2lVKbu6/fkvh82Dtv7aHiP7jXQSLkxaDyfZFQY7ZAEwyOS9
NqTHZcSqDB4E8TuDu2Q=
-----END CERTIFICATE-----
+28
View File
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQChKCzGbuI2o0ro
xXBd5tMJQMJbeHGp3huYb8dH1XF8inx5AMFsk17+r6w6YQbYPoI008y46V6uoYlC
erMbzec2ER1WMwV2SAgSzZeNL4KA2Nc/jja1gCJsvFf/1QFAJg4MDVE2/rl1E0/E
wJFCsyR7ZvqdTb7kTAFMWjEhDmi1qs088NG7X27nKolDDFwLrNnew2ar+M4uRPL2
D4kOsxHJ20CtXpjotSxkFNQ9Plwo76Z9AZ7sPtvn5gF271MjhinMbr/tnd2UR6EE
mJaktJ0oPYWU3+OJ2I2v+6yWlMjF0RWYrvfjYtm5c6XAR1VKw/yVp+fNXda0f+dF
nFnuukM3AgMBAAECggEAHvMCNlGRLAEMH8vbGo17tekz6jElUwQOtKWy1b7N/qbx
5KMagmmC8tJ19vgvQlQu2UpbpJ9cyZjILXY2834Fi0KaKbW+tMp8km/dTQX0wB3k
qJX9N7oV4mABXmZfyRwF0pa0JCWjJDLDPPvmabLRrMlIE/GcWWenUCx6Buknfsuy
bT0z0SG3b9fiQYp5o+vUknsHeW6NOJP9cvS6yLw8nH2keO97ldqQw074xs0IFlHo
5Vecoeuv+Kr+EPxJpWovWHRVoJrXDsH95wQ+eqR15H9PMrzWKE5ilzT54/V3tJ+V
840865hndaSF0LaF1huNAc088FyrgLM6MnOTFc5nAQKBgQDTxAI6cwsLfFwyed1f
XRAWj6XBlCwAdh+rIzz2WTS5XQnN0bBof5cjY3pgZW3guG2cKs5TtW4iZKYvgQel
nmO+DTiIMBJFmnTeT7PLdszvbtgncrABc5h92BRFuCdJOTyxVyo+RH4mUWvuUgzl
fOzGCMgTDj997ayCOtJQ9V38QQKBgQDC0eguxL0rNMvv1hBz8ri9pe9u5c252C1L
QxiWv0bUkOVxldVk5sZwNdvntGLKX0J5JiIrAUYWyPSWcizxdj+8XKZ+Qrj1d4tQ
Fxex27ZINTOprPI8DFAxk1N3coQ0XtQ1ANAYgqzTVt0N2ceV6JR5mrNaRFAuxFRV
GiFYEZ3BdwKBgH1aR59lviH8aW8PqDTtF7rjKVFfbnSPd8uMPmftAkK/ijAlcfFJ
paOzHRu2rsYZJfkNdbkjaqA9SApSrL4i9hPoK5CiNA6zkX6PW3STTFhnGtivEPSr
ApdrswRaMBcuJHktibn2PV5dUDBwrWWi/11ykUyGYJsgd79XJmv58rqBAoGBAJ7k
ajSyuwJhJi9u7K5UeT4b9wMApGB9ortudsCEMrWtk6SnKosH+TKNitQYeDZLqUB7
zIQ8nrh4n5uQdX1eDy+GdTgnHQj+vf3NR8MEyk9sLL2nU02MUV+NpBaYrKC18udj
yVUkRuZ7QwFH7JMkROxFMyBc9PSfpZRfInDctUiTAoGBAK5NmzGicDEqcyD4U2Dx
nxc5t0KP2DtqfMDWAL/hTZsV1hCq1QsQ5jmzbVq3tM1BUDDMoNwxm47ycQaD8vhb
SByA8660UEmMYX3C1Dx6N+N768ca6GSM1sOGCngl9ldObhdX0xIUYZXcn33EIq4e
/NhzHj/po4W1yBVlRUhaVPT2
-----END PRIVATE KEY-----
@@ -0,0 +1,38 @@
ssl_certificate /etc/certs/${NGINX_CERT};
ssl_certificate_key /etc/certs/${NGINX_CERT_KEY};
# Zunächst wird ein Default-Server definiert.
# Das Standardverhalten von NGINX ist nämlich wie folgt:
# Wenn im Request ein Server-Name reinkommt und NGINX keinen Server-Block findet, in dem dieser
# Servername definiert wird, wird einfach der erste definierte Serverblock genommen.
# Dadurch, dass ein expliziter Default-Server definiert wird, wird dieser verwendet.
# Falls jetzt also ein Servername übergeben wird, den es nicht gibt, gibt NGINX den Error-Code 404 zurück
server {
listen 4430 ssl default_server;
return 404;
}
server {
listen 4430 ssl;
server_name ${NGINX_SERVER_NAME};
resolver 127.0.0.11 valid=30s; # notwendig, damit der Upstream (s.u.) nicht direkt den nginx-Server stoppt, falls pythonapp noch nicht soweit ist
# https://sandro-keil.de/blog/let-nginx-start-if-upstream-host-is-unavailable-or-down/
location /static {
root /usr/share/nginx/html;
}
location / {
# Variable muss benutzt werden (s.o. bei resolver Direktive)
set $upstream pythonapp:8080;
proxy_pass http://$upstream;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /health-check {
add_header Content-Type text/plain;
return 200 "success $time_iso8601";
}
}
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

+22
View File
@@ -0,0 +1,22 @@
.w3-theme-l5 {color:#000 !important; background-color:#f9f9f9 !important}
.w3-theme-l4 {color:#000 !important; background-color:#ececec !important}
.w3-theme-l3 {color:#000 !important; background-color:#d8d8d8 !important}
.w3-theme-l2 {color:#000 !important; background-color:#c5c5c5 !important}
.w3-theme-l1 {color:#000 !important; background-color:#b1b1b1 !important}
.w3-theme-d1 {color:#fff !important; background-color:#8e8e8e !important}
.w3-theme-d2 {color:#fff !important; background-color:#7e7e7e !important}
.w3-theme-d3 {color:#fff !important; background-color:#6f6f6f !important}
.w3-theme-d4 {color:#fff !important; background-color:#5f5f5f !important}
.w3-theme-d5 {color:#fff !important; background-color:#4f4f4f !important}
.w3-theme-light {color:#000 !important; background-color:#f9f9f9 !important}
.w3-theme-dark {color:#fff !important; background-color:#4f4f4f !important}
.w3-theme-action {color:#fff !important; background-color:#4f4f4f !important}
.w3-theme {color:#fff !important; background-color:#9e9e9e !important}
.w3-text-theme {color:#9e9e9e !important}
.w3-border-theme {border-color:#9e9e9e !important}
.w3-hover-theme:hover {color:#fff !important; background-color:#9e9e9e !important}
.w3-hover-text-theme:hover {color:#9e9e9e !important}
.w3-hover-border-theme:hover {border-color:#9e9e9e !important}
+23
View File
@@ -0,0 +1,23 @@
/* fallback */
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal;
font-weight: 400;
src: url(Google_Symbols.woff2) format('woff2');
}
.material-icons {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
+235
View File
@@ -0,0 +1,235 @@
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
article,aside,details,figcaption,figure,footer,header,main,menu,nav,section{display:block}summary{display:list-item}
audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline}
audio:not([controls]){display:none;height:0}[hidden],template{display:none}
a{background-color:transparent}a:active,a:hover{outline-width:0}
abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}
b,strong{font-weight:bolder}dfn{font-style:italic}mark{background:#ff0;color:#000}
small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}
code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible}
button,input,select,textarea,optgroup{font:inherit;margin:0}optgroup{font-weight:bold}
button,input{overflow:visible}button,select{text-transform:none}
button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}
button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}
button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}
fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}
legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}
[type=checkbox],[type=radio]{padding:0}
[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}
[type=search]{-webkit-appearance:textfield;outline-offset:-2px}
[type=search]::-webkit-search-decoration{-webkit-appearance:none}
::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
/* End extract */
html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden}
h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}
.w3-serif{font-family:serif}.w3-sans-serif{font-family:sans-serif}.w3-cursive{font-family:cursive}.w3-monospace{font-family:monospace}
h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px}
hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit}
.w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc}
.w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1}
.w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1}
.w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center}
.w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top}
.w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px}
.w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap}
.w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)}
.w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none}
.w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none}
.w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%}
.w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none}
.w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block}
.w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s}
.w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%}
.w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc}
.w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer}
.w3-dropdown-hover:hover .w3-dropdown-content{display:block}
.w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000}
.w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000}
.w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1}
.w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px}
.w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto}
.w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%}
.w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%}
.w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px}
.w3-main,#main{transition:margin-left .4s}
.w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)}
.w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px}
.w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto}
.w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0}
.w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left}
.w3-bar .w3-button{white-space:normal}
.w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0}
.w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%}
.w3-responsive{display:block;overflow-x:auto}
.w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before,
.w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both}
.w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%}
.w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%}
.w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%}
.w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%}
@media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%}
.w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%}
.w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}}
@media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%}
.w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%}
.w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}}
.w3-rest{overflow:hidden}.w3-stretch{margin-left:-16px;margin-right:-16px}
.w3-content,.w3-auto{margin-left:auto;margin-right:auto}.w3-content{max-width:980px}.w3-auto{max-width:1140px}
.w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell}
.w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom}
.w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important}
@media (max-width:1205px){.w3-auto{max-width:95%}}
@media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px}
.w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative}
.w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center}
.w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}}
@media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}}
@media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}}
@media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}}
@media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}.w3-auto{max-width:100%}}
.w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0}
.w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2}
.w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0}
.w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0}
.w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)}
.w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)}
.w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)}
.w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)}
.w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none}
.w3-display-position{position:absolute}
.w3-circle{border-radius:50%}
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
.w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)}
.w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)}
.w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}
.w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}}
.w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}}
.w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}}
.w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}}
.w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}}
.w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}}
.w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}}
.w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important}
.w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1}
.w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75}
.w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)}
.w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)}
.w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)}
.w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important}
.w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important}
.w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important}
.w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important}
.w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important}
.w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important}
.w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important}
.w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important}
.w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important}
.w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important}
.w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important}
.w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important}
.w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important}
.w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important}
.w3-padding-64{padding-top:64px!important;padding-bottom:64px!important}
.w3-padding-top-64{padding-top:64px!important}.w3-padding-top-48{padding-top:48px!important}
.w3-padding-top-32{padding-top:32px!important}.w3-padding-top-24{padding-top:24px!important}
.w3-left{float:left!important}.w3-right{float:right!important}
.w3-button:hover{color:#000!important;background-color:#ccc!important}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important}
/* Colors */
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
.w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important}
.w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important}
.w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important}
.w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important}
.w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important}
.w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important}
.w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important}
.w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important}
.w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important}
.w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important}
.w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important}
.w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important}
.w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important}
.w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important}
.w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important}
.w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important}
.w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important}
.w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important}
.w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important}
.w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important}
.w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important}
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
.w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important}
.w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important}
.w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important}
.w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important}
.w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important}
.w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important}
.w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important}
.w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important}
.w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important}
.w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important}
.w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important}
.w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important}
.w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important}
.w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important}
.w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important}
.w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important}
.w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important}
.w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important}
.w3-text-red,.w3-hover-text-red:hover{color:#f44336!important}
.w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important}
.w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important}
.w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important}
.w3-text-white,.w3-hover-text-white:hover{color:#fff!important}
.w3-text-black,.w3-hover-text-black:hover{color:#000!important}
.w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important}
.w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important}
.w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important}
.w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important}
.w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important}
.w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important}
.w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important}
.w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important}
.w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important}
.w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important}
.w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important}
.w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important}
.w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important}
.w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important}
.w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important}
.w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important}
.w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important}
.w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important}
.w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important}
.w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important}
.w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important}
.w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important}
.w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important}
.w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important}
.w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important}
.w3-border-black,.w3-hover-border-black:hover{border-color:#000!important}
.w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important}
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
+32
View File
@@ -0,0 +1,32 @@
name: kbpbase
services:
nginx-proxy:
build:
context: NGINX_Server
args: # Übergibt die ARGS an das dockerfile
NGINX_VERSION: ${NGINX_VERSION}
ports:
- ${NGINX_HOST_PORT}:4430
volumes:
- ${NGINX_STATIC_DIR}:/usr/share/nginx/html:ro
- ${NGINX_CONFIG_TEMPLATE_DIR}:/etc/nginx/templates
- ${NGINX_CERTS}:/etc/certs
environment:
- NGINX_CERT=${NGINX_CERT}
- NGINX_CERT_KEY=${NGINX_CERT_KEY}
- NGINX_SERVER_NAME=${NGINX_SERVER_NAME}
pythonapp:
build:
context: App_Server
# target: kommt aus dem prod oder dev dockerfile
args: # Übergibt die ARGS an das dockerfile
PYTHON_VERSION: ${PYTHON_VERSION}
volumes:
#- ${PYTHON_APP_DIR}:/usr/opt/pythonapp/root
- ${PYTHON_DB_DIR}:/usr/var/pythonappdb
- python-venv:/usr/opt/pythonapp/env
# - ${PYTHON_ENV_DIR}:/usr/opt/pythonapp/env
volumes:
python-venv:
+19
View File
@@ -0,0 +1,19 @@
name: kbpdev
services:
pythonapp:
extends:
file: docker-compose.base.yaml
service: pythonapp
build:
target: dev
args: # Übergibt die ARGS an das dockerfile
# PYTHON_VERSION: ${PYTHON_VERSION}
NODE_VERSION: ${NODE_VERSION:-18}
NPM_VERSION: ${NPM_VERSION:-8}
volumes:
- ${PYTHON_DEV_DIR}:/usr/opt/pythonapp
# - ${TYPESCRIPT_SRC_DIR}:/usr/opt/typescript
- node-modules:/usr/opt/typescript/node_modules
volumes:
node-modules:
+10
View File
@@ -0,0 +1,10 @@
name: kbpprod
services:
pythonapp:
extends:
file: docker-compose.base.yaml
service: pythonapp
build:
target: prod
volumes:
- ${PYTHON_APP_DIR}:/usr/opt/pythonapp
+26
View File
@@ -0,0 +1,26 @@
services:
nginx-proxy:
build:
context: NGINX_Server
args: # Übergibt die ARGS an das dockerfile
NGINX_VERSION: ${NGINX_VERSION}
ports:
- ${NGINX_HOST_PORT}:4430
volumes:
- ${NGINX_STATIC_DIR}:/usr/share/nginx/html:ro
- ${NGINX_CONFIG_TEMPLATE_DIR}:/etc/nginx/templates
- ${NGINX_CERTS}:/etc/certs
environment:
- NGINX_CERT=${NGINX_CERT}
- NGINX_CERT_KEY=${NGINX_CERT_KEY}
- NGINX_SERVER_NAME=${NGINX_SERVER_NAME}
pythonapp:
build:
context: App_Server
args: # Übergibt die ARGS an das dockerfile
PYTHON_VERSION: ${PYTHON_VERSION}
volumes:
- ${PYTHON_APP_DIR}:/usr/opt/pythonapp/root
- ${PYTHON_DB_DIR}:/usr/var/pythonappdb
# - ${PYTHON_ENV_DIR}:/usr/opt/pythonapp/env