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