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:
1 parent
10549934ea
commit
1ba78992a6
63 files changed
+5410
No files matched your search
@@ -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"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
__pycache__
|
||||||
|
env
|
||||||
|
node_modules
|
||||||
|
tsout
|
||||||
|
kbp_crm*.js
|
||||||
|
App_Server/data/
|
||||||
Vendored
+103
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
*/node_modules
|
||||||
|
*/tsout
|
||||||
|
*/env
|
||||||
|
*/data
|
||||||
@@ -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"]
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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()
|
||||||
@@ -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')
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
@@ -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")
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[DEBUG]
|
||||||
|
debug=True
|
||||||
|
|
||||||
|
[APP]
|
||||||
|
projectname=kbp_crm
|
||||||
|
version=00.00.01
|
||||||
|
flasksecret=Hier_muss_ein_sicherer_schluessel_eingesetzt_werden
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
SQLAlchemy
|
||||||
|
Flask
|
||||||
|
Flask-SQLAlchemy
|
||||||
|
Flask-Login
|
||||||
|
waitress
|
||||||
|
# pyodbc
|
||||||
@@ -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"}
|
||||||
@@ -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()
|
||||||
@@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
@@ -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 "$@"
|
||||||
@@ -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"),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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[]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export interface iRole {
|
||||||
|
id: number
|
||||||
|
roleid: string
|
||||||
|
name: string
|
||||||
|
level: number
|
||||||
|
beschreibung: string
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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))
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export class BaseView {
|
||||||
|
|
||||||
|
getattr(vnode: any, attrname: string, defaultvalue: any) {
|
||||||
|
return vnode.attrs[attrname] === undefined ? defaultvalue : vnode.attrs[attrname]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
|
})
|
||||||
@@ -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)
|
||||||
Generated
+1299
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
]
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": false,
|
||||||
|
"outDir": "./tsout",
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ES2019",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
@@ -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-----
|
||||||
@@ -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 |
@@ -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}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
@@ -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:
|
||||||
@@ -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:
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user