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

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

No files matched your search

+616
View File
@@ -0,0 +1,616 @@
import m from "mithril"
import { Treetable, iTreetable, iTreenode, Treenode, TopbarButton, iTopbarButton, changeTreenodes } from "../widgets/Treetable_V2"
import {ModalType, ModalView, iModalView} from "../widgets/Modal"
import { iApplication} from "./models/applications"
import { iRole } from "./models/roles"
import {gridMountpoints} from "../mainlayout"
import {WrongEditSelectionMessage, iWrongEditSelectionMessage, showServerError} from "./authmessages"
const MOVERIGHT = "Die markierten und nicht markierten Rollen werden den markierten Anwendungen zugeordnet"
const localDataModel = {
applist: [] as iApplication[],
rolelist: [] as iRole[],
appnodes: [] as iTreenode[],
rolenodes: [] as iTreenode[],
loadData: () => {
Promise.all([
m.request({
method: "GET",
url: "/auth/getapplications"
}).then((result) => localDataModel.applist = result as iApplication[]),
m.request({
method: "GET",
url: "/auth/getroles"
}).then((result) => {localDataModel.rolelist = result as iRole[]})
])
.then (() => {
localDataModel.appnodes = []
localDataModel.rolenodes = []
localDataModel.applist.map((app: iApplication) => {
let appnode: iTreenode = new Treenode(app, false, [])
app.roles.forEach((role: iRole) => {
let newChildnode = new Treenode(role)
newChildnode.isChecked = true
appnode.children.push(newChildnode)
})
appnode.children.sort(sortRoleNodes)
localDataModel.appnodes.push(appnode)
})
localDataModel.rolelist.map((role: iRole) => {
let rolenode: iTreenode = new Treenode(role, false, [])
localDataModel.rolenodes.push(rolenode)
})
localDataModel.rolenodes.sort(sortRoleNodes)
changeTreenodes(localDataModel.appnodes, true)
})
},
updateRole: (role: iRole) => {
m.request ({
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
url: "/auth/updaterole",
body: role
})
.catch((e) => {showServerError(e)})
.finally(() => localDataModel.loadData())
},
deleteRoles: (roles: iRole[]) => {
let roleids: string[] = []
roles.forEach((role: iRole) => {
roleids.push(role.roleid)
})
m.request({
method: "POST",
url: "/auth/deleteroles",
body: roleids
})
.catch((e) => {showServerError(e)})
.finally(() => localDataModel.loadData())
},
updateApplication: (app: iApplication) => {
m.request ({
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
url: "/auth/updateapplication",
body: app
})
.catch((e) => {showServerError(e)})
.finally(() => localDataModel.loadData())
},
deleteApplications: (apps: iApplication[]) => {
let appids: string[] = []
apps.forEach((app: iApplication) => {
appids.push(app.appid)
})
m.request({
method: "POST",
url: "/auth/deleteapplications",
body: appids
})
.catch((e) => {showServerError(e)})
.finally(() => localDataModel.loadData())
},
updateApplicationRoles: () => {
interface iApplicationRole {
appid: string
roles: {roleid: string, isChecked: boolean}[]
}
let applicationroles: iApplicationRole[] = []
localDataModel.appnodes.forEach((appnode: iTreenode) => {
let app: iApplication = appnode.nodevalue
let approle: iApplicationRole = {
appid: app.appid,
roles: []
}
appnode.children.forEach((rolenode: iTreenode) => {
let role: iRole = rolenode.nodevalue
approle.roles.push({roleid: role.roleid, isChecked: rolenode.isChecked})
})
applicationroles.push(approle)
})
m.request({
method: "POST",
url: "/auth/updateapplicationroles",
body: applicationroles
})
.catch((e) => {showServerError(e)})
.finally(() => localDataModel.loadData())
}
}
export const applicationpage: m.ClosureComponent = (vnode) => {
return {
oninit: (vnode) => {
localDataModel.loadData()
},
view: (vnode) => {
return m("div.w3-container.w3-content", {
style: {'max-width': '80%', display: 'flex', 'flex-direction': 'column', height: 'inherit', 'overflow-y': 'inherit'}
},
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
m("div.w3-cell-row",
m("div.w3-cell.w3-padding.w3-mobile",
m("h1", "Rollen"),
m(Treetable,
{
treenodes: localDataModel.rolenodes,
startexpanded: false,
onRowMarked: (node: iTreenode) => {
//alert(node.nodevalue['roleid'])
},
columnmapper: (node: iTreenode) => {
let role = node.nodevalue as iRole
return [
m("span.w3-button",
{
style: {padding: '0px'},
onclick: () => {
node.isChecked = !node.isChecked
}
},
m("i.material-icons.w3-theme-light", node.isChecked?"check_box":"check_box_outline_blank")
),
m("div", role.level),
m("div", role.roleid),
m("div", role.name)
]
}
} as iTreetable,
m(TopbarButton,
{
onclick: applyRoles,
tooltip: MOVERIGHT
} as iTopbarButton,
m("i.material-icons", "keyboard_double_arrow_right")
),
m(TopbarButton,
{
onclick: () => {m.mount(gridMountpoints.modalhook, {
view: (vnode) => {
return m(RoleForm, {role: {id: -1, level: 0, name: "", roleid: ""} as iRole})
}
})}
} as iTopbarButton,
m("i.material-icons", "add")
),
m(TopbarButton,
{
onclick: () => {m.mount(gridMountpoints.modalhook, DeleteRolesMessage)}
} as iTopbarButton,
m("i.material-icons", "delete_forever")
),
m(TopbarButton,
{
onclick: () => {
let roles: iRole[] = []
localDataModel.rolenodes.forEach((rolenode: Treenode) => {
if (rolenode.isChecked) {
roles.push(rolenode.nodevalue)
}
})
if (roles.length != 1) {
m.mount(gridMountpoints.modalhook, WrongRoleEditSelectionMessage)
}
else {
m.mount(gridMountpoints.modalhook, {
view: (vnode) => {
return m(RoleForm, {
role: roles[0] as iRole
})
}
})
}
}
} as iTopbarButton,
m("i.material-icons", "edit")
)
)
),
m("div.w3-cell.w3-padding.w3-mobile",
m("h1", "Anwendungen"),
m(Treetable,
{
treenodes: localDataModel.appnodes,
startexpanded: true,
columnmapper: (node: iTreenode) => {
let cols: any[] = []
// Hier wird "Duck-Typing" genutzt, um den Typ von node.nodevalue zu ermitteln
// Je nach Typ werden die Spalten der Tabelle dann anders befüllt
if (node.nodevalue['appid'] != undefined) {
let app = node.nodevalue as iApplication
cols[0] = "enabled"
cols[1] = app.appid
cols[2] = app.name
}
else if (node.nodevalue['roleid'] != undefined) {
let role = node.nodevalue as iRole
cols[0] = "disabled"
cols[1] = role.roleid
cols[2] = role.name
}
return [
m(cols[0] == "enabled"?"span.w3-button":"span",
{
style: {padding: '0px'},
onclick: () => {
if (cols[0] == "enabled") {
node.isChecked = !node.isChecked
}
}
},
m("i.material-icons.w3-theme-light", node.isChecked?"check_box":"check_box_outline_blank")
),
m("div", cols[1]),
m("div", cols[2])
]
}
} as iTreetable,
m(TopbarButton,
{
onclick: applyRoles,
tooltip: MOVERIGHT
} as iTopbarButton,
m("i.material-icons", "keyboard_double_arrow_right")
),
m(TopbarButton,
{
onclick: () => {m.mount(gridMountpoints.modalhook,
{
view: (vnode) => {
return m(ApplicationForm, {application: {id: -1} as iApplication} as iApplicationForm)
}
}
)}
} as iTopbarButton,
m("i.material-icons", "add")
),
m(TopbarButton,
{
onclick: () => {m.mount(gridMountpoints.modalhook, DeleteApplicatonsMessage)}
} as iTopbarButton,
m("i.material-icons", "delete_forever")
),
m(TopbarButton,
{
onclick: () => {
let apps: iApplication[] = []
localDataModel.appnodes.forEach((appnode: Treenode) => {
if (appnode.isChecked) {
apps.push(appnode.nodevalue)
}
})
if (apps.length != 1) {
m.mount(gridMountpoints.modalhook, WrongApplicationEditSelectionMessage)
}
else {
m.mount(gridMountpoints.modalhook, {
view: (vnode) => {
return m(ApplicationForm, {
application: apps[0] as iApplication
} as iApplicationForm)
}
})
}
}
} as iTopbarButton,
m("i.material-icons", "edit")
)
)
)
)
)
)
}
}
}
function sortRoleNodes(node1: iTreenode, node2: iTreenode) {
// es soll zuerst nach Level und dann nach rowid sortiert werden
let v1 = (node1.nodevalue['level'].toString().padStart(6,'0') + node1.nodevalue['roleid']).toLocaleLowerCase('de-de')
let v2 = (node2.nodevalue['level'].toString().padStart(6,'0') + node2.nodevalue['roleid']).toLocaleLowerCase('de-de')
if (v1 < v2) {
return -1
}
if (v1 > v2) {
return 1
}
return 0
}
function applyRoles() {
let checkedApps = localDataModel.appnodes.filter((appnode: iTreenode) => {
return appnode.isChecked
})
if (checkedApps.length == 0) {
m.mount(gridMountpoints.modalhook, ErrorMessage)
return
}
m.mount(gridMountpoints.modalhook, {view: () => {return m(ApplyRolesWarning, {applyroles: doit})}})
function doit() {
checkedApps.forEach((appnode: iTreenode) => {
localDataModel.rolenodes.forEach((rolenode: iTreenode) => {
let approle: iTreenode = appnode.children.find((child: iTreenode) => {
return child.nodevalue['roleid'] === rolenode.nodevalue['roleid']
})
if (approle != undefined) {
approle.isChecked = rolenode.isChecked
}
else if (rolenode.isChecked) {
let newapprole = new Treenode(rolenode.nodevalue, false, [])
newapprole.isChecked = rolenode.isChecked
appnode.children.push(newapprole)
}
})
})
localDataModel.updateApplicationRoles()
}
}
const ErrorMessage: m.Component = {
view: (vnode) => {
return (
m(ModalView,
{
title: "Fehler",
type: ModalType.ERROR,
width: '33%'
} as iModalView,
m("p", "Es wurden noch keine Ziel-Anwendungen markiert!"),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "OK"),
]
)
)
)
}
}
export interface iApplyRolesWaring {
applyroles: Function
}
const ApplyRolesWarning: m.Component<iApplyRolesWaring> = {
view: (vnode) => {
return (
m(ModalView,
{
title: "Hinweis",
type: ModalType.MESSAGE,
width: '33%'
} as iModalView,
m("p", "Wenn Rollen nicht ausgewählt sind bedeutet dies, dass die Rollen in den ausgewählten Anwendungen gelöscht werden!"),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "Abbruch"),
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null); vnode.attrs.applyroles()}}, "OK"),
]
)
)
)
}
}
const WrongRoleEditSelectionMessage: m.Component = {
view: () => {
return m(WrongEditSelectionMessage, {elementname: "Rolle"} as iWrongEditSelectionMessage)
}
}
const WrongApplicationEditSelectionMessage: m.Component = {
view: () => {
return m(WrongEditSelectionMessage, {elementname: "Anwendung"} as iWrongEditSelectionMessage)
}
}
const DeleteRolesMessage: m.ClosureComponent = (vnode) => {
let roles: iRole[]
return {
oninit: (vnode) => {
roles = []
localDataModel.rolenodes.forEach((rolenode: iTreenode) => {
if (rolenode.isChecked) {
roles.push(rolenode.nodevalue)
}
})
},
view: (vnode) => {
return (
m(ModalView,
{
title: "Rollen löschen",
type: ModalType.MESSAGE,
width: '33%'
} as iModalView,
m("p", "Sie wollen die folgenden Rollen löschen:"),
m("ul",
roles.map((role: iRole) => {
return m("li", role.roleid)
})
),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right",
{
style: {'margin-left': '4px'},
onclick: () => {
localDataModel.deleteRoles(roles);
m.mount(gridMountpoints.modalhook, null)
}}, "Löschen"),
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "Abbrechen"),
]
)
)
)
}
}
}
interface iRoleForm {
role: iRole
}
const RoleForm: m.ClosureComponent<iRoleForm> = () => {
let form: HTMLFormElement = null;
let currentrole: iRole
function reset (e) {
form.reset()
}
function unmount () {
m.mount(gridMountpoints.modalhook, null)
}
function submit (e) {
e.preventDefault()
let formdata: FormData = new FormData(e.target as HTMLFormElement)
for (let element of formdata) {
currentrole[element[0]] = element[1]
}
localDataModel.updateRole(currentrole)
unmount()
}
return {
view: (vnode) => {
currentrole = (vnode.attrs.role === undefined) ? {} as iRole : vnode.attrs.role
return m(ModalView,
{
title: "Rolle",
type: ModalType.MESSAGE,
width: 'max-content'
} as iModalView,
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: submit},
m("label", "ID (intern)"),
m("div.w3-padding-small", currentrole.id),
m("label", "Rollen-ID"),
m("input.w3-input.w3-border", {name: 'roleid', type: 'text', value: currentrole.roleid}),
m("label", "Name"),
m("input.w3-input.w3-border", {name: 'name', type: 'text', value: currentrole.name}),
m("label", "Level"),
m("input.w3-input.w3-border", {name: 'level', type: 'number', value: currentrole.level}),
m("label", "Beschreibung"),
m("input.w3-input.w3-border", {name: 'beschreibung', type: 'text', value: currentrole.beschreibung}),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Speichern"),
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, onclick: unmount, type: "button"}, "Abbrechen"),
m("button.w3-button.w3-theme.w3-right", {onclick: reset, type: "button"}, "Zurücksetzen")
]
)
)
)
}
}
}
interface iApplicationForm {
application: iApplication
}
const ApplicationForm: m.ClosureComponent<iApplicationForm> = () => {
let form: HTMLFormElement = null;
let currentapp: iApplication
let isDirty = false
function reset (e) {
form.reset()
}
function unmount () {
if (isDirty) {
alert("is Dirty")
}
m.mount(gridMountpoints.modalhook, null)
}
function submit (e) {
e.preventDefault()
let formdata: FormData = new FormData(e.target as HTMLFormElement)
for (let element of formdata) {
currentapp[element[0]] = element[1]
}
localDataModel.updateApplication(currentapp)
unmount()
}
return {
view: (vnode) => {
currentapp = (vnode.attrs.application === undefined) ? {} as iApplication : vnode.attrs.application
return m(ModalView,
{
title: "Anwendung",
type: ModalType.MESSAGE,
width: 'max-content'
} as iModalView,
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: submit},
m("label", "ID (intern)"),
m("div.w3-padding-small", currentapp.id),
m("label", "Application-ID"),
m("input.w3-input.w3-border", {name: 'appid', type: 'text', value: currentapp.appid}),
m("label", "Name"),
m("input.w3-input.w3-border", {name: 'name', type: 'text', value: currentapp.name}),
m("label", "Minimum-Version"),
m("input.w3-input.w3-border", {name: 'minversion', type: 'text', value: currentapp.minversion}),
m("label", "Beschreibung"),
m("input.w3-input.w3-border", {name: 'beschreibung', type: 'text', value: currentapp.beschreibung}),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Speichern"),
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, onclick: unmount, type: "button"}, "Abbrechen"),
m("button.w3-button.w3-theme.w3-right", {onclick: reset, type: "button"}, "Zurücksetzen")
]
)
)
)
}
}
}
const DeleteApplicatonsMessage: m.ClosureComponent = (vnode) => {
let apps: iApplication[]
return {
oninit: (vnode) => {
apps = []
localDataModel.appnodes.forEach((appnode: iTreenode) => {
if (appnode.isChecked) {
apps.push(appnode.nodevalue)
}
})
},
view: (vnode) => {
return (
m(ModalView,
{
title: "Anwendungen löschen",
type: ModalType.MESSAGE,
width: '33%'
} as iModalView,
m("p.w3-panel.w3-red",
[
"Vor dem Löschen muss sichergestellt sein, dass es noch mindestens eine App gibt, in der die Rolle 'SysAdmin' ",
"vergeben ist und in der ein Benutzer diese Rolle besitzt!"
]
),
m("p", "Sie wollen die folgenden Anwendungen löschen:"),
m("ul",
apps.map((app: iApplication) => {
return m("li", app.appid)
})
),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right",
{
style: {'margin-left': '4px'},
onclick: () => {
localDataModel.deleteApplications(apps);
m.mount(gridMountpoints.modalhook, null)
}}, "Löschen"),
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "Abbrechen"),
]
)
)
)
}
}
}
@@ -0,0 +1,40 @@
import m from "mithril"
import { get_menustack, menubar } from "../header"
import { userlist } from "./userpage"
import { applicationpage } from "./applicationpage"
let mountpoint = null
function close_page() {
get_menustack().pop()
m.route.set("/blankpage")
}
function show_userlist() {
m.mount(mountpoint, userlist)
}
function show_applist() {
m.mount(mountpoint, applicationpage)
}
export const appmanagementpage: m.Component = {
oncreate: (vnode) => {
mountpoint = vnode.dom
},
oninit: (vnode) => {
get_menustack().push(menu)
},
view: (vnode) => {
return m("div", m(userlist))
}
}
const menu: m.Component = {
view: (vnode) => {
return m(menubar, [
m("a.w3-button", {onclick: show_userlist}, "Benutzer"),
sessionStorage.getItem("CurrentUserIsSysAdmin") != null ?
m("div.w3-button", {onclick: show_applist}, "Anwendungen"):null,
m("div.w3-button.w3-right", {onclick: close_page}, "Beenden"),
])
}
}
+76
View File
@@ -0,0 +1,76 @@
import m from "mithril"
import { gridMountpoints } from "../mainlayout"
import {ModalType, ModalView, iModalView} from "../widgets/Modal"
export interface iWrongEditSelectionMessage{
elementname: string
}
export const WrongEditSelectionMessage: m.Component<iWrongEditSelectionMessage> = {
view: (vnode) => {
return (
m(ModalView,
{
title: "Falsche Auswahl",
type: ModalType.ERROR,
width: '33%'
} as iModalView,
m("p", `Zum Editieren muss genau 1 ${vnode.attrs.elementname} ausgewählt sein!`),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "OK"),
]
)
)
)
}
}
export interface iServerErrorMessage {
error: {
messages: string[],
status: number
}
}
export const ServerErrorMessage: m.Component<iServerErrorMessage> = {
view: (vnode) => {
return (
m(ModalView,
{
title: "Fehler",
type: ModalType.ERROR,
width: "33%"
} as iModalView,
m("div.w3-panel",
vnode.attrs.error.messages.map((message) => {
return m("div", message)
}),
m("div.w3-small", `(HTML-Status: ${vnode.attrs.error.status})`)
),
m("div.w3-bar.w3-padding-16",
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "OK"),
)
)
)
}
}
export function showServerError (e) {
let servererror:iServerErrorMessage = {error: {messages:[], status: e.code}}
console.log(JSON.stringify(e), e.message)
if (e.response != undefined && e.response["messages"] != undefined) {
if (!Array.isArray(e.response["messages"])) {
e.response["messages"] = [e.response["messages"]]
}
servererror.error.messages = e.response.messages
}
else if (e.response != undefined) {
servererror.error.messages = [JSON.stringify(e.response)]
}
else {
servererror.error.messages = [JSON.stringify(e)]
}
m.mount(gridMountpoints.modalhook, {
view: () => {return m(ServerErrorMessage, {error: servererror.error } as iServerErrorMessage)}
})
}
+117
View File
@@ -0,0 +1,117 @@
import m from "mithril";
import {ModalView, iModalView} from "../widgets/Modal"
import {iUser} from "./models/users"
import {showServerError} from "./authmessages"
import { headerdata } from "../header";
export interface iLoginscreen {
setnextpage: string
}
export const loginscreen: m.ClosureComponent<iLoginscreen> = (vnode) => {
let form: HTMLFormElement = null;
function reset (e) {
form.reset()
}
function checklogin(e: Event) {
e.preventDefault()
let formdata: FormData = new FormData(e.target as HTMLFormElement)
let user: iUser
m.request ({
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
url: "/auth/checklogin",
body: Object.fromEntries(formdata.entries())
})
.then((result) => {
user = result as iUser;
// global.appglobals.currentuser = user.username
// global.appglobals.currentuserid = user.userid
sessionStorage.setItem("CurrentUser", user.username);
headerdata.username = user.username
headerdata.userid = user.userid
// nur wenn es sich um einen SysAdmin handelt, wird der SessionStorage gesetzt. Will man später wissen,
// ob der aktuelle User ein SysAdmin ist, braucht man nur auf das Vorhandensein des Schlüssels abzufragen
// und nicht auch noch auf den Inhalt
if (user.isSysAdmin) {
sessionStorage.setItem("CurrentUserIsSysAdmin", "1");
}
})
.then(() => {m.route.set(vnode.attrs.setnextpage)})
.catch((error) => {
sessionStorage.removeItem("CurrentUser");
sessionStorage.removeItem("CurrentUserIsSysAdmin");
showServerError(error)
})
}
return {
oninit: (vnode) => {
m.request({
method: "GET",
url: "/auth/logout"
})
.then (() => {
sessionStorage.removeItem("CurrentUser");
sessionStorage.removeItem("CurrentUserIsSysAdmin");
// global.appglobals.currentuser = "nicht angemeldet"
// global.appglobals.currentuserid = ""
})
},
view: (vnode) => {
return m(ModalView, {title: "Login", width: '33%'} as iModalView,
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: checklogin},
m("label", "Benutzer-ID"),
m("input.w3-input.w3-border", {name: 'userid', type: 'text'}),
m("label", "Kennwort"),
m("input.w3-input.w3-border", {name: 'password', type: 'password'}),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Login"),
m("button.w3-button.w3-theme.w3-right", {onclick: reset, type: "button"}, "Zurücksetzen"),
]
)
)
)
}
}
}
export const changepasswordscreen: m.ClosureComponent = () => {
let form: HTMLFormElement = null;
function reset (e) {
form.reset()
}
function changepassword(e) {
e.preventDefault()
let formdata: FormData = new FormData(e.target as HTMLFormElement)
m.request ({
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
url: "/auth/changepassword",
body: Object.fromEntries(formdata.entries())
})
.then(() => {m.route.set("/login")})
.catch((e) => {showServerError(e)})
}
return {
view: (vnode) => {
return m(ModalView, {title: "Kennwort ändern", width: '33%'} as iModalView,
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: changepassword},
m("label", "Benutzer-ID"),
m("input.w3-input.w3-border", {name: 'Userid', type: 'text', value: headerdata.userid, readonly: true}),
m("label", "Altes Kennwort"),
m("input.w3-input.w3-border", {name: 'OldPassword', type: 'password'}),
m("label", "Neues Kennwort"),
m("input.w3-input.w3-border", {name: 'NewPassword1', type: 'password'}),
m("label", "Neues Kennwort"),
m("input.w3-input.w3-border", {name: 'NewPassword2', type: 'password'}),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Kennwort ändern"),
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, onclick: reset, type: "button"}, "Zurücksetzen"),
m("button.w3-button.w3-theme.w3-right", {onclick: () => {history.back()}, type: "button"}, "Abbrechen"),
]
)
)
)
}
}
}
@@ -0,0 +1,18 @@
import {iRole} from "./roles"
export interface iApplication {
id: number
appid: string
name: string
minversion: string
beschreibung: string
roles: iRole[]
}
export interface iUserApp {
id: number
userid: number
appid: number
userapp_roles : iRole[]
}
+8
View File
@@ -0,0 +1,8 @@
export interface iRole {
id: number
roleid: string
name: string
level: number
beschreibung: string
}
+24
View File
@@ -0,0 +1,24 @@
import { iUserApp } from "./applications"
import { iRole } from "./roles"
export interface iUser {
id: number
userid: string
username: string
password: string
email: string
telefon: string
isSysAdmin: boolean
userapps: iUserApp[]
}
export function hasAppRole(user: iUser, appid: number, roleid: number) {
let userapp: iUserApp = user.userapps.find((userapp: iUserApp) => {return userapp.appid == appid})
if (userapp != undefined) {
let userapprole: iRole = userapp.userapp_roles.find((role: iRole) => {return role.id == roleid})
if (userapprole != undefined) {
return true
}
}
return false
}
+364
View File
@@ -0,0 +1,364 @@
import m from "mithril"
import {hasAppRole, iUser} from "./models/users"
import {iApplication} from "./models/applications"
import {iRole} from "./models/roles"
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "../widgets/Treetable_V2"
import {gridMountpoints, maingrid} from "../mainlayout"
import {ModalView, ModalType, iModalView} from "../widgets/Modal"
import { iWrongEditSelectionMessage, WrongEditSelectionMessage, showServerError} from "./authmessages"
interface iTabelrow {
rowtype: any
col1: any
cols: any[]
info?: {
parentuserid: number
parentappid: number
roleid: number
ischecked: boolean
}
}
const localDataModel = {
userlist: [] as iUser[],
applist: [] as iApplication[],
usernodes: [] as iTreenode[],
startexpanded: false,
loadData: () => {
Promise.all([
m.request({
method: "GET",
url: "/auth/getusers"
}).then((result) => localDataModel.userlist = result as iUser[]),
m.request({
method: "GET",
url: "/auth/getapplications"
}).then((result) => localDataModel.applist = result as iApplication[]),
])
.then(() => {
localDataModel.usernodes = []
localDataModel.userlist.map((u) => {
let userrow: iTabelrow = {
rowtype: 'user',
col1: u.userid,
cols: [u.username, u.email, u.telefon]
}
let usernode: iTreenode = new Treenode(userrow, localDataModel.startexpanded, [])
localDataModel.applist.forEach((app: iApplication) => {
let approw: iTabelrow = {
rowtype: "app",
col1: app.appid,
cols: [app.name, app.minversion],
info: {
ischecked: false,
parentuserid: u.id,
parentappid: null,
roleid: null
}
}
let appnode: Treenode = new Treenode(approw, localDataModel.startexpanded, [])
app.roles.forEach((role: iRole) => {
let ischecked = hasAppRole(u, app.id, role.id)
let rolerow: iTabelrow = {
rowtype: "role",
col1: role.name,
cols: [role.roleid],
info: {
parentuserid: u.id,
parentappid: app.id,
ischecked: ischecked,
roleid: role.id
}
}
let rolenode: iTreenode = new Treenode(rolerow, localDataModel.startexpanded, [])
appnode.children.push(rolenode)
})
usernode.children.push(appnode)
})
localDataModel.usernodes.push(usernode)
})
})
.catch((e) => {
if (e.code == 401) {
m.route.set("/login")
}
})
},
updateUser: (user: iUser) => {
m.request ({
method: "POST", // Hier muss POST verwendet werden, weil GET keinen Body hat und Parameter nur im URL transportieren kann
url: "/auth/updateuser",
body: user
})
.catch((e) => {showServerError(e)})
.finally(() => localDataModel.loadData())
},
deleteUsers: (users: iUser[]) => {
m.request({
method: "POST",
url: "/auth/deleteusers",
body: users.map((u: iUser) => {return u.userid})
})
.catch((e) => {showServerError(e)})
.finally(() => localDataModel.loadData())
},
applyUserAppRole: (tablerow: iTabelrow) => {
m.request ({
method: "POST",
url: "/auth/applyuserapprole",
body: tablerow.info
})
.catch((e) => {showServerError(e); localDataModel.loadData()})
}
}
export const userlist: m.ClosureComponent = (vnode) => {
return {
oninit: (vnode) => {
localDataModel.loadData()
},
view: (vnode) => {
return m("div.w3-container.w3-content", {style: {'max-width': 'min-content', display: 'flex', 'flex-direction': 'column', height: 'inherit', 'overflow-y': 'inherit'}},
m("h1", "Benutzerliste"),
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
m(Treetable,
{
startexpanded: localDataModel.startexpanded,
treenodes: localDataModel.usernodes,
columnmapper: (node: iTreenode) => {
let row: iTabelrow = node.nodevalue
let isChecked = hasCheckedChilds(node)
if (row.rowtype == "user") {
return [
m("strong", row.col1),
m("strong", row.cols[0]),
m(isChecked?"div.w3-text-green":"div.w3-text-red", {style: {display: 'flex'}},
// m("div", {style: {padding: '0px', height:'24px', width: '24px'}},
// m("i.material-icons", isChecked?null:"dangerous"),
// ),
m("div", isChecked?"aktiv":"inaktiv")
),
m("div.w3-small",
m("div", row.cols[1]),
m("div", row.cols[2])
),
m(EditUserButton, {userid: row.col1} as iEditUserButton),
m(DeleteUserButton, {userid: row.col1} as iDeleteUserButton)
]
}
else if (row.rowtype == "role") {
return [
m("span.w3-small", row.col1),
m("span.w3-button", {
style: {padding: '0px'},
onclick: () => {
row.info.ischecked = !row.info.ischecked
localDataModel.applyUserAppRole(row)
}
},
m("i.material-icons.w3-theme-light", row.info.ischecked?"check_box":"check_box_outline_blank")
),
m("span.w3-small", row.cols[0]),
m("span.w3-small", row.cols[1]),
m("span", ""),
m("span", "")
]
}
else {
// rowtype "app"
let isChecked = hasCheckedChilds(node)
return [
m(isChecked?"span":"strong.w3-text-red", row.col1),
m("div", {style: {padding: '0px', height:'24px', width: '24px'}},
m("i.material-icons", isChecked?"check":null)
),
m("span", row.cols[0]),
m("span", row.cols[1]),
m("span", ""),
m("span", "")
]
}
}
} as iTreetable,
m(TopbarButton, {
onclick: () => {
m.mount(gridMountpoints.modalhook, {
view: (vnode) => {
return m(UserForm, {user: {id: -1} as iUser})
}
})
}} as iTopbarButton,
m("i.material-icons", "add")
)
)
)
)
}
}
}
function hasCheckedChilds(parenttreenode: iTreenode): boolean {
return parenttreenode.children.some((treenode: iTreenode) => {
if (treenode.hasChildren) {
return hasCheckedChilds(treenode)
}
let tablerow = treenode.nodevalue as iTabelrow
return tablerow.info.ischecked
})
}
interface iUserForm {
user: iUser
}
const UserForm: m.ClosureComponent<iUserForm> = () => {
let form: HTMLFormElement = null;
let currentuser: iUser
function reset (e) {
form.reset()
}
function unmount () {
m.mount(gridMountpoints.modalhook, null)
}
function submit (e) {
e.preventDefault()
let formdata: FormData = new FormData(e.target as HTMLFormElement)
for (let element of formdata) {
currentuser[element[0]] = element[1]
}
localDataModel.updateUser(currentuser)
unmount()
}
return {
oninit: (vnode) => {
currentuser = (vnode.attrs.user === undefined) ? {} as iUser : vnode.attrs.user
},
view: (vnode) => {
return m(ModalView,
{
title: "Benutzer",
type: ModalType.MESSAGE,
width: 'max-content'
} as iModalView,
m("form.w3-padding-16", {oncreate: (vnode) => {form = vnode.dom as HTMLFormElement}, onsubmit: submit},
m("label", "ID (intern)"),
m("div.w3-padding-small", currentuser.id),
m("label", "Benutzer-ID"),
m("input.w3-input.w3-border", {name: 'userid', type: 'text', value: currentuser.userid}),
m("label", "Name"),
m("input.w3-input.w3-border", {name: 'username', type: 'text', value: currentuser.username}),
m("label", "E-Mail"),
m("input.w3-input.w3-border", {name: 'email', type: 'email', value: currentuser.email}),
m("label", "Telefon"),
m("input.w3-input.w3-border", {name: 'telefon', type: 'tel', value: currentuser.telefon}),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, type: "submit"}, "Speichern"),
m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, onclick: unmount, type: "button"}, "Abbrechen"),
m("button.w3-button.w3-theme.w3-right", {onclick: reset, type: "button"}, "Zurücksetzen")
]
)
)
)
}
}
}
const WrongUserEditSelectionMessage: m.Component = {
view: () => {
return m(WrongEditSelectionMessage, {elementname: "Benutzer"} as iWrongEditSelectionMessage)
}
}
interface iDeleteUsersMessage {
userid: string
}
const DeleteUsersMessage: m.ClosureComponent<iDeleteUsersMessage> = (vnode) => {
let users: iUser[]
return {
oninit: (vnode) => {
users = localDataModel.userlist.filter((u: iUser) => {
return u.userid == vnode.attrs.userid
})
},
view: (vnode) => {
return (
m(ModalView,
{
title: "Benutzer löschen",
type: ModalType.MESSAGE,
width: '33%'
} as iModalView,
m("p", "Sie wollen den folgenden Benutzer löschen:"),
m("ul",
users.map((user: iUser) => {
return m("li", `${user.userid} (${user.username})`)
})
),
m("div.w3-bar.w3-padding-16",
[
m("button.w3-button.w3-theme.w3-right",
{
style: {'margin-left': '4px'},
onclick: () => {
localDataModel.deleteUsers(users);
m.mount(gridMountpoints.modalhook, null)
}}, "Löschen"),
m("button.w3-button.w3-theme.w3-right", {onclick: () => {m.mount(gridMountpoints.modalhook, null)}}, "Abbrechen"),
]
)
)
)
}
}
}
interface iDeleteUserButton {
userid: string
}
export const DeleteUserButton: m.ClosureComponent<iDeleteUserButton> = () => {
return {
view: (vnode) => {
return m("button.w3-button.w3-bar-item", {
style: {'min-width': '24px', height: '24px', padding: '0'},
onclick: () => {
m.mount(gridMountpoints.modalhook, {view: () => {
return m(DeleteUsersMessage, {userid: vnode.attrs.userid} as iDeleteUsersMessage)
}})
},
title: "Benutzer löschen"
},
m("i.material-icons", "delete_forever")
)
}
}
}
interface iEditUserButton {
userid: string
}
export const EditUserButton: m.ClosureComponent<iEditUserButton> = () => {
return {
view: (vnode) => {
return m("button.w3-button.w3-bar-item", {
style: {'min-width': '24px', height: '24px', padding: '0'},
onclick: () => {
let users: iUser[] = localDataModel.userlist.filter((u:iUser) => {
return u.userid == vnode.attrs.userid
})
m.mount(gridMountpoints.modalhook, {view: () => {
return m(UserForm, {user: users[0]} as iUserForm)
}})
},
title: "Benutzer bearbeiten"
},
m("i.material-icons", "edit")
)
}
}
}