UNPKG

easy-web-app

Version:
1,436 lines (1,325 loc) 48.6 kB
/*! * easy-web-gui * Copyright(c) 2018 ma-ha * MIT Licensed */ var gui = exports = module.exports = { pages : {} ,userTokens : {} ,pullDownMenu : {} ,appRoot: '/' ,decor: 'decor' }; // logger var log = require( 'npmlog' ); // use express for REST services var express = require( 'express' ); var webservices = express(); var router = express.Router(); gui.express = webservices var jwt = require( 'jsonwebtoken' ) var bodyParser = require( 'body-parser' ); // body-parser for JSON payload: var jsonParser = bodyParser.json(); // body-parser for parsing application/x-www-form-urlencoded: var formParser = bodyParser.urlencoded( { extended: true } ); var cookieParser = require( 'cookie-parser' ) webservices.use( cookieParser() ) // support external configuration var config = require( 'config' ) if ( config.staging ) { log.info( 'Web GUI', 'config stage: '+config.staging ) } var cfg = {} if ( config.has('easy-web-app') ) { cfg = config.get( 'easy-web-app' ) if ( cfg.enableCustomCSS ) { router.use( '/css-custom', express.static( process.cwd()+'/css' ) ) log.info( 'Web GUI', 'use '+process.cwd() +'/custom.css' ) } if ( cfg['img-cust'] ) { router.use( '/img-cust', express.static( process.cwd() +'/'+ cfg['img-cust'] ) ) log.info( 'Web GUI', 'get "/img-cust" from folder '+process.cwd() +'/'+ cfg['img-cust'] ) } } log.info( 'Web GUI', 'config port: '+cfg.port ) if ( config.loglevel ) { log.info( 'Web GUI', 'switch to log level: '+config.loglevel ) log.level = config.loglevel } /** Initialize the Web GUI */ gui.init = function init( logoText, port, rootPath, options ) { this.appRoot = rootPath || cfg.rootPath || '/' if ( this.appRoot.indexOf('/') != 0 ) { this.appRoot = '/'+this.appRoot } var mainPage = this.setDefaults( options ) mainPage.header.logoText = cfg.logoText || logoText mainPage.title = cfg.title || mainPage.title mainPage.footer.copyrightText = cfg.copyrightText || mainPage.footer.copyrightText if ( cfg.logoURL ) { mainPage.setLogoURL( cfg.logoURL ) } var wsPort = port || cfg.port || 8888 if ( typeof WEB_SERVER_PORT !== 'undefined' ) { wsPort = WEB_SERVER_PORT // global setting override } webservices.use( this.appRoot, router ) webservices.listen( wsPort ) log.info( 'Web GUI', 'http://localhost:' + wsPort + this.appRoot ) if ( cfg.enableSecurity === true ) { gui.enableSecurity() if ( cfg.loginTimeout ) { gui.loginTimeout = cfg.loginTimeout } } return mainPage } gui.getExpress = function getExpress() { return router; } /** Set defaults for all required configurations */ gui.setDefaults = function setDefaults( options ) { // create a default "main" page minimum config var navUrl = ( this.appRoot=='/' ? '/svc/nav' : this.appRoot+'/svc/nav' ) this.pages[ 'main' ] = { title : 'Main Page' , version: '2' , header : { logoText : 'Test' ,frameWarning: "true" ,modules : [ ] } , rows : [] , footer : { copyrightText : 'powered by '+ '<a href="https://github.com/ma-ha/rest-web-ui">ReST-Web-GUI</a>' , modules: [] } } this.pages[ 'main' ].setLogo = function( text, imgLink ) { if ( ! text ) return this.header['logo'] = { text: text, url: gui.appRoot + '/index.html' } if ( imgLink ) this.header['logo']['img'] = imgLink } this.pages[ 'main' ].setLogoText = function( text ) { this.header['logoText'] = text } this.pages[ 'main' ].setLogoURL = function( url ) { this.header['logoURL'] = url } this.pages[ 'main' ].setTitle = function( text ) { this.title = text } this.pages[ 'main' ].addColumnsRow = function ( id, height ) { return gui.addColumnsRow( id, this.rows, height ) } this.pages[ 'main' ].addView = function ( def, config ) { if ( def.id ) { def.id = def.id.replace( ' ', '' ) } def.rowId = def.rowId || def.id; return gui.addViewIn( def, config, this.rows ) } this.pages[ 'main' ].addTabContainer = function( def ) { return addTabCont( def, 'row', this.rows ) } /** set page width attribute and override CSS */ this.pages[ 'main' ].setPageWidth = function setPageWidth( width ) { this[ 'page_width' ] = width } this.pages[ 'main' ].setCopyright = function( text ) { this.footer['copyrightText'] = text } this.pages[ 'main' ].addFooterLink = function( linkText, linkURL, linkTarget ) { if ( ! this.footer['linkList'] ) { this.footer['linkList'] = [] } if ( linkTarget ) { this.footer['linkList'].push( { text:linkText, url:linkURL, target:linkTarget } ) } else { this.footer['linkList'].push( { text:linkText, url:linkURL } ) } } this.pages[ 'main' ].addInfo = function( text ) { this.info = text } this.pages[ 'main' ].delInfo = function( ) { this.info = null } this.pages[ 'main' ].dynamicRow = function( dynamicRowCallback ) { this.dynamicRowCallback = dynamicRowCallback } if ( options && options.nav == 'embedded' ) { var navView = { id : 'Nav', title:'', type:'pong-nav-embed', resourceURL:'/svc/nav-embed', height:'XX', decor:"none" } this.pages[ 'main' ].addView ( navView ) options.decor = 'none' this.pages[ 'main' ].header.embedNav = true } else { var navbar = { id : 'MainNav' , type : 'pong-navbar' , param : { confURL : navUrl } } this.pages[ 'main' ].header.modules.push( navbar ) } if ( options && options.decor != 'none' ) { this.pages[ 'main' ].decor = gui.decor } return this.pages[ 'main' ] } gui.dynamicTitle = function( dynamicTitleCallback ) { this.dynamicTitleCallback = dynamicTitleCallback } gui.dynamicHeader = function( dynamicHeaderCallback ) { this.dynamicHeaderCallback = dynamicHeaderCallback } gui.dynamicFooter = function( dynamicFooterCallback ) { this.dynamicFooterCallback = dynamicFooterCallback } function addTabCont( def, type, arr ) { var tabDiv = {} tabDiv.tabs = [] if ( type == 'row' ) { tabDiv.height = def.height || '400px' tabDiv.rowId = def.rowId || def.id } else { tabDiv.width = def.width || '100%' tabDiv.columnId = def.columnId || def.id } tabDiv.addView = ( def, vConfig ) => { var view = { tabId : def.id, title : def.title || def.id || ' ', resourceURL : def.resourceURL || null } if ( def.type ) { view.type = def.type } if ( def. actions ) { view.actions = def.actions } if ( vConfig ) { view.moduleConfig = vConfig } tabDiv.tabs.push( view ) return view } tabDiv.addRows = ( id, title ) => { var rows = gui.addRowsColumn( id, null, '100%' ) rows.title = title rows.tabId = id delete rows.columnId delete rows.width tabDiv.tabs.push( rows ) return rows } arr.push( tabDiv ) return tabDiv } /** add new pull down menu */ gui.addPullDownMenu = function addPullDownMenu( menuId, menuLabel ) { var newMenu = { id: menuId, type: 'pong-pulldown', moduleConfig: { title: menuLabel, menuItems : [ ] } } this.pullDownMenu[ menuId ] = newMenu this.pages[ 'main' ].header.modules.push( newMenu ) } /** add a new pull down menu header module */ gui.addPullDownMenuHtmlItem = function addPullDownMenuHtmlItem( menuId, htmlString ) { if ( this.pullDownMenu[ menuId ] ) { this.pullDownMenu[ menuId ].moduleConfig.menuItems.push( { html:htmlString } ) } else { log.warn( 'addPullDownMenuHtmlItem: Menu "'+menuId+'" not available!') } } gui.dynamicNav = function dynamicNav( dynamicNavCallback ) { this.dynamicNavCallback = dynamicNavCallback } /** add new page to portal, navigation tabs included */ gui.addPage = function addPage( pageId, title, viewDef, viewConfig ) { if ( ! pageId ) { log.warn( 'gui.addPage', 'pageID not defined properly' ); } else if ( this.pages[ pageId ] ) { log.warn( 'gui.addPage', 'Page "' + pageId + '" already exists in GUI.' ); } else { var pgObj = { title : pageId , version: '2' , header : this.pages[ 'main' ].header , rows : [] , footer : this.pages[ 'main' ].footer } if ( title ) pgObj.title = title pgObj.addColumnsRow = function ( id, height) { // log.info( 'pages[].addColumns', id ) return gui.addColumnsRow( id, this.rows, height ) } pgObj.addView = function ( def, config ) { def.rowId = def.rowId || def.id; if ( def.rowId ) { def.rowId = def.rowId.replace( ' ','' ) } return gui.addViewIn( def, config, pgObj.rows ) } pgObj.addTabContainer = function( def ) { return addTabCont( def, 'row', pgObj.rows ) } /** set page width attribute and override CSS */ pgObj.setPageWidth = function setPageWidth( width ) { this[ 'page_width' ] = width } pgObj.addInfo = function( text ) { this.info = text } pgObj.delInfo = function( ) { this.info = null } pgObj.dynamicRow = function( dynamicRowCallback ) { this.dynamicRowCallback = dynamicRowCallback } if ( this.pages[ 'main' ].header.embedNav ) { var navView = { id : 'Nav', title:'', type:'pong-nav-embed', resourceURL:'/svc/nav-embed', height:'XX', decor:"none" } pgObj.addView ( navView ) } pgObj.addSubNav = function( ) { var navView = { id : 'SubNav', title:'', type:'pong-nav-embed', resourceURL:'/svc/nav-embed-sub', height:'XX', decor:"none" } pgObj.addView ( navView ) } if ( viewDef ) { if ( viewDef.id ) { viewDef.id = viewDef.id.replace( ' ', '' ) } pgObj.addView( viewDef, viewConfig ) } this.pages[ pageId ] = pgObj // check if pag is for pull down menu: if ( pageId.indexOf( '/' ) != -1 ) { var menuId = pageId.substr( 0 , pageId.indexOf( '/' ) ) // log.info( ' ... '+menuId ) if ( menuId == 'user' ) { if ( gui.secParams ) { // only if security is enabled //log.info( 'menu "'+menuId +'": '+pageId) if ( ! gui.secParams.userPages ) { gui.secParams.userPages = {} } gui.secParams.userPages[ title ] = pageId } } else if ( this.pullDownMenu[ menuId ] ) { // log.info( 'pullDownMenu: '+menuId ) this.pullDownMenu[ menuId ].moduleConfig.menuItems.push( { pageLink:pageId, label:title } ) } } return pgObj } } /** split the column into a rows, to add views */ gui.addColumnsRow = function addColumnsRow( id, colArr, height ) { // log.info( 'addColumns', id ) var rowObj = { rowId : id , height: height || "300px" , cols : [] } rowObj.addRowsColumn = function ( id, width ) { return gui.addRowsColumn( id, this.cols, width ) } rowObj.addView = function ( def, config ) { if ( def.id && ! def.columnId ) { def.columnId = def.id } if ( def.columnId ) { def.columnId = def.columnId .replace( ' ', '' ) } return gui.addViewIn( def, config, this.cols ) } rowObj.addTabContainer = function( def ) { return addTabCont( def , 'col', rowObj.cols ) } colArr.push( rowObj ) // log.info( 'addColumns', 'rowObj='+JSON.stringify( rowObj ) ) return rowObj } /** split the row into a columns, to add views */ gui.addRowsColumn = function addRowsColumn( id, cols, width ) { var newCol = { columnId: id , width: width || "300px" , rows : [] } newCol.addColumnsRow = function ( id, height) { // log.info( 'pages[].addColumns', id ) return gui.addColumnsRow( id, this.rows, height ) } newCol.addRows = function ( height ) { return gui.addColumns( this.rows, height ) } newCol.addView = function ( def, config ) { if ( def.id && ! def.rowId ) { def.rowId = def.id } if ( def.rowId ) { def.rowId = def.rowId.replace( ' ', '' ) } return gui.addViewIn( def, config, this.rows ) } newCol.addTabContainer = function( def ) { return addTabCont( def , 'row', this.rows ) } if ( cols ) cols.push( newCol ) return newCol } gui.addViewIn = function addViewIn( def, config, arr ) { var view = JSON.parse( JSON.stringify( def ) ) // clone it if ( def.title == '' ) { view.title = null } else { view.title = def.title || def.id || "View:" } view.decor = def.decor || this.decor view.resourceURL = def.resourceURL || "none" view.height = def.height || '400px' if ( config ) { view.moduleConfig = config } if ( view.id ) { view.id = view.id.replace( ' ', '' ) } arr.push( view ) return view } /** add a view (new row) to the page */ gui.addView = function addView( def, config, page ) { var pg = page || 'main'; // pg = encodeURIComponent ( pg ); var view = {}; if ( !this.pages[ pg ] ) { log.warn( 'gui.addView', 'Page "' + pg + '" not found in GUI!' ); return null; } if ( !def ) { log.warn( 'gui.addView', '"def" is required parameter!' ); return null; } if ( !def.id ) { log.warn( 'gui.addView','"def.id" is required!' ); return null; } else { // OK, add view view[ 'rowId' ] = def.id.replace( ' ', '' ) ; view[ 'title' ] = def.title || def.id || "View:"; view[ 'decor' ] = def.decor || this.decor; view[ 'height' ] = def.height || '400px'; view[ 'resourceURL' ] = def.resourceURL || "none"; if ( def.type ) { view[ 'type' ] = def.type; } if ( config ) { view[ 'moduleConfig' ] = config; } if (def. actions ) { view[ 'actions' ] = def.actions; } this.pages[ pg ].rows.push ( view ); } // console.log( JSON.stringify( this.pages[ pg ] ) ); return view; }; /* define static directories to load the framework into the web page */ var fs = require('fs') var staticDir = __dirname + '/rest-web-gui/html' if ( fs.existsSync( staticDir ) ) { log.info(' Init', 'Using static in '+staticDir ) } else { var path =require('path') staticDir = path.resolve( __dirname, '../rest-web-gui/html' ) if ( fs.existsSync( staticDir ) ) { log.info(' Init', 'Using static in '+staticDir ) } else { staticDir = __dirname + '/node_modules/rest-web-gui/html' if ( fs.existsSync( staticDir ) ) { log.info(' Init', 'Using static in '+staticDir ) } else { log.error(' Init', 'No path to "rest-web-gui" found! ' ) log.error(' Init', 'Tried: '+__dirname + '/rest-web-gui/html' ) log.error(' Init', 'Tried: '+path.resolve( __dirname, '../rest-web-gui/html' ) ) log.error(' Init', 'Tried: '+staticDir ) process.exit(1) } } } router.use ( '/css', express.static( staticDir + '/css' ) ); router.use ( '/js', express.static( staticDir + '/js' ) ); router.use ( '/img', express.static( staticDir + '/img' ) ); router.use ( '/modules', express.static( staticDir + '/modules' ) ); // router.use ( '/i18n', express.static( staticDir + '/i18n' ) ); function getBasePageName( page ) { var len2 = page.length -2 if ( page.indexOf( '-m' ) == len2 || page.indexOf( '-t' ) == len2 ) { return page.substring( 0, len2 ) } else { return page } } /** REST web service to GET layout structure: */ router.get( '/svc/layout/:id/structure', async function( req, res ) { var pgId = getBasePageName( req.params.id ) var userId = null try { userId = await gui.getUserIdFromReq( req ) } catch( exc ) { log.warn( 'easy-web-app /svc/layout/:id/structure', exc ) } if ( gui.authorize && ! await gui.authorize( userId, pgId, req ) ) { // not authorized for this page var redirectPage = 'main' if ( gui.secParams && gui.secParams.needLoginPage ) { redirectPage = gui.secParams.needLoginPage } var pg = JSON.parse( JSON.stringify( gui.pages[ redirectPage ] ) ) // cloned if ( gui.authorize && pg.header ) { // check authorization for header modules var user = await gui.getUserIdFromReq( req ) for ( var i = pg.header.modules.length-1; i >= 0; i-- ) { //log.info( '>>>>', pg.header.modules[i].type) let allowedModules = [ 'pong-security', 'pong-security2', 'pong-navbar' ] if ( allowedModules.indexOf( pg.header.modules[i].type ) == -1 ) { // all others should be checked for authorization if ( pg.header.modules[i].id && ! await gui.authorize( user, pg.header.modules[i].id, req ) ) { delete pg.header.modules[i] // not a } } } } pg = await dynamicStructureCallbacks( pg, gui.pages[ redirectPage ].dynamicRowCallback, req, pgId ) var layout = { 'layout' : pg } // console.log( layout ) return res.json( layout ) } else if ( gui.pages[ req.params.id ] ) { var pg = JSON.parse( JSON.stringify( gui.pages[ req.params.id ] ) ) // cloned if ( gui.authorize && pg.header ) { // check authorization for header modules var user = await gui.getUserIdFromReq( req ) for ( var i = pg.header.modules.length-1; i >= 0; i-- ) { let allowedModules = [ 'pong-security', 'pong-security2', 'pong-navbar' ] if ( allowedModules.indexOf( pg.header.modules[i].type ) == -1 ) { // all others should be checked for authorization if ( pg.header.modules[i].id && ! await gui.authorize( user, pg.header.modules[i].id, req ) ) { delete pg.header.modules[i] // not authorized } } } } pg = await dynamicStructureCallbacks( pg, gui.pages[ req.params.id ].dynamicRowCallback, req, pgId ) //log.info( "structure", req.params.id ) var layout = { 'layout' : pg } return res.json( layout ) } else { res.statusCode = 404 return res.send( 'Error 404: No quote found' ) } } ); /** REST web service to GET layout structure for sub menu pages: */ router.get( '/svc/layout/:id/:subid/structure', async function( req, res ) { var page = req.params.id +'/'+ req.params.subid // console.log( '>>'+req.params.subid ); var userId = null try { userId = await gui.getUserIdFromReq( req ) } catch( exc ) { log.warn( 'easy-web-app /svc/layout/:id/:subid/structure', exc ) } if ( gui.authorize && ! gui.authorize( userId, page, req ) ) { // not authorized for this page var redirectPage = 'main' if ( gui.secParams && gui.secParams.needLoginPage ) { redirectPage = gui.secParams.needLoginPage } var pg = JSON.parse( JSON.stringify( gui.pages[ redirectPage ] ) ) // cloned if ( gui.authorize && pg.header ) { // check authorization for header modules var user = await gui.getUserIdFromReq( req ) for ( var i = pg.header.modules.length-1; i >= 0; i-- ) { //log.info( '>>>>', pg.header.modules[i].type) let allowedModules = [ 'pong-security', 'pong-security2', 'pong-navbar' ] if ( allowedModules.indexOf( pg.header.modules[i].type ) == -1 ) { // all others should be checked for authorization if ( pg.header.modules[i].id && ! gui.authorize( user, pg.header.modules[i].id, req ) ) { delete pg.header.modules[i] // not a } } } } pg = await dynamicStructureCallbacks( pg, gui.pages[ redirectPage ].dynamicRowCallback, req, page ) var layout = { 'layout' : pg }; return res.json( layout ); } else if ( gui.pages[ page ] ) { var pg = JSON.parse( JSON.stringify( gui.pages[ page] ) ) // cloned pg = await dynamicStructureCallbacks( pg, gui.pages[ page ].dynamicRowCallback, req, page ) var layout = { 'layout' : pg }; res.json( layout ); } else { res.statusCode = 404; return res.send( 'Error 404: Page "'+page+'" not found' ); } } ); // enable to replace the static structure of header, rows and footer per request using callbacks async function dynamicStructureCallbacks( pg, dynamicRowCallback, req, page ) { if ( gui.dynamicTitleCallback ) { // manipulate title on the fly try { let newTitle = await gui.dynamicTitleCallback( pg.title, req, page ) if ( newTitle ) { pg.title = newTitle } } catch( exc ) { log.warn( 'easy-web-app /svc/layout/:id/structure dynamicRowCallback', exc ) } } if ( gui.dynamicHeaderCallback ) { // manipulate header content on the fly via callback try { let newHeader = await gui.dynamicHeaderCallback( pg.header, req, page ) if ( newHeader ) { pg.header = newHeader } } catch( exc ) { log.warn( 'easy-web-app /svc/layout/:id/structure dynamicRowCallback', exc ) } } if ( dynamicRowCallback ) { // generate rows content on the fly via callback try { let newRows = await dynamicRowCallback( pg.rows, req, page ) if ( newRows ) { pg.rows = newRows } } catch( exc ) { log.warn( 'easy-web-app /svc/layout/:id/structure dynamicRowCallback', exc ) } } if ( gui.dynamicFooterCallback ) { // manipulate footer content on the fly via callback try { let newFooter = await gui.dynamicFooterCallback( pg.footer, req, page ) if ( newFooter ) { pg.footer = newFooter } } catch( exc ) { log.warn( 'easy-web-app /svc/layout/:id/structure dynamicRowCallback', exc ) } } return pg } /** * Single page does it all, the layout parameter references the "page". Default * is the "main" page */ router.get ( '/', function( req, res ) { if ( gui.appRoot == '/' ) { res.redirect( '/index.html' ); } else { res.redirect( gui.appRoot+'/index.html' ); } } ); router.get( '/index.html', function( req, res ) { res.sendFile( __dirname + '/index.html' ); } ); /** web service to assemble top level menu items for embedded navigation bar */ router.get( '/svc/nav-embed', async function( req, res ) { var navTabs = [] var layout = req.query.page // main menu for ( var layoutId in gui.pages ) { if ( gui.pages.hasOwnProperty ( layoutId ) ) { if (( layoutId.indexOf( '-nonav' ) == -1 || layoutId.indexOf( '-nonav' ) != layoutId.length -6 ) && ( layoutId.indexOf( '-m' ) == -1 || layoutId.indexOf( '-m' ) != layoutId.length -2 ) && ( layoutId.indexOf( '-t' ) == -1 || layoutId.indexOf( '-t' ) != layoutId.length -2 ) && ( layoutId.indexOf( 'user/' ) != 0 ) ) { // check authorization for page if ( gui.authorize && ! await gui.authorize( userId, layoutId, req ) ) { // not visible for this user } else if ( layoutId == 'main' && gui.pages['main'].header.logo ) { // not displayed, because header link } else { var nav = { 'layout' : layoutId, 'label' : ( gui.pages[ layoutId ].navLabel ? gui.pages[ layoutId ].navLabel : gui.pages[ layoutId ].title ) } if ( gui.pages[ layoutId ].info ) { nav.info = gui.pages[ layoutId ].info } navTabs.push( nav ) } } } } if ( gui.dynamicNavCallback ) { // generate nav on the fly via callback try { let newNavTabs = await gui.dynamicNavCallback( 'nav-embed', navTabs, req ) if ( newNavTabs ) { navTabs = newNavTabs } } catch( exc ) { log.warn( 'easy-web-app /svc/nav-embed dynamicNavCallback', exc ) } } res.json( { 'navigations' : navTabs } ) } ) /** web service to assemble sub level menu items for embedded navigation bar */ router.get( '/svc/nav-embed-sub', async function( req, res ) { var navTabs = [] var layout = req.query.page var masterPage = layout + '/' if ( layout && layout.indexOf( '/' ) > 0 ){ masterPage = layout.substr( 0, ( layout.indexOf( '/' )+1) ) } //log.info( 'nav-embed-sub', masterPage ) for ( var layoutId in gui.pages ) { //log.info( 'nav-embed-sub', layoutId +' -> '+layoutId.indexOf( masterPage ) ) if ( layoutId.indexOf( masterPage ) == 0 ) { if ( ( layoutId.indexOf( '-nonav' ) == -1 || layoutId.indexOf( '-nonav' ) != layoutId.length -6 ) && ( layoutId.indexOf( '-m' ) == -1 || layoutId.indexOf( '-m' ) != layoutId.length -2 ) && ( layoutId.indexOf( '-t' ) == -1 || layoutId.indexOf( '-t' ) != layoutId.length -2 ) && ( layoutId.indexOf( 'user/' ) != 0 ) ) { // check authorization for page if ( gui.authorize && ! await gui.authorize( userId, layoutId, req ) ) { // not visible for this user } else if ( layoutId == 'main' && gui.pages['main'].header.logo ) { // not displayed, because header link } else { var nav = { 'layout' : layoutId, 'label' : ( gui.pages[ layoutId ].navLabel ? gui.pages[ layoutId ].navLabel : gui.pages[ layoutId ].title ) } if ( gui.pages[ layoutId ].info ) { nav.info = gui.pages[ layoutId ].info } navTabs.push( nav ) } } } } if ( gui.dynamicNavCallback ) { // generate nav on the fly via callback try { let newNavTabs = await gui.dynamicNavCallback( 'nav-embed-sub', navTabs, req ) if ( newNavTabs ) { navTabs = newNavTabs } } catch( exc ) { log.warn( 'easy-web-app /svc/nav-embed-sub dynamicNavCallback', exc ) } } res.json( { 'navigations' : navTabs } ) } ) /** web service for multi page navigation bar */ router.get( '/svc/nav', async function( req, res ) { try { var navTabs = [] var subMenus = {} var userId = await gui.getUserIdFromReq( req ) // console.log( 'GET /svc/nav '+gui.pages.length ) for ( var layoutId in gui.pages ) { // console.log( '>>'+layoutId ) if ( gui.pages.hasOwnProperty ( layoutId ) ) { if ( layoutId.indexOf( '/' ) == -1 ) { // ignore alternate mobile and tablet layouts if ( ( layoutId.indexOf( '-nonav' ) == -1 || layoutId.indexOf( '-nonav' ) != layoutId.length -6 ) && ( layoutId.indexOf( '-m' ) == -1 || layoutId.indexOf( '-m' ) != layoutId.length -2 ) && ( layoutId.indexOf( '-t' ) == -1 || layoutId.indexOf( '-t' ) != layoutId.length -2 ) ) { // check authorization for page if ( gui.authorize && ! await gui.authorize( userId,layoutId, req ) ) { // not visible for this user } else if ( layoutId == 'main' && gui.pages['main'].header.logo ) { // not displayed, because header link } else { let nav = { 'layout' : layoutId, 'label' : ( gui.pages[ layoutId ].navLabel ? gui.pages[ layoutId ].navLabel : gui.pages[ layoutId ].title ) } if ( gui.pages[ layoutId ].navId ) { nav.id = gui.pages[ layoutId ].navId } else { nav.id = layoutId.replace(/[\W_]+/g, '' ) } if ( gui.pages[ layoutId ].navHTML ) { nav.html = gui.pages[ layoutId ].navHTML } if ( gui.pages[ layoutId ].info ) { nav.info = gui.pages[ layoutId ].info } navTabs.push( nav ) } } } else { // sub-menu let subMenu = layoutId.substring( 0 , layoutId.indexOf( '/' ) ) if ( ! gui.pullDownMenu[ subMenu ] && subMenu != 'user' ) { // if this is not a pull down if ( ! subMenus[ subMenu ] ) { // first sub menu item creates menu // ignore alternate mobile and tablet layouts if ( layoutId.indexOf( '-m' ) != layoutId.length -2 && layoutId.indexOf( '-t' ) != layoutId.length -2 ) { if ( gui.authorize && ! await gui.authorize( userId, layoutId, req ) ) { // not visible for this user } else { subMenus[ subMenu ] = navTabs.length let nav = { label : subMenu, menuItems: [] } nav.id = subMenu.replace(/[\W_]+/g, '' ) navTabs.push( nav ) } } } if ( ( gui.authorize && ! await gui.authorize( userId, layoutId, req ) ) ) { // not visible for this user } else if ( subMenu == 'user' ) { //log.verbose( 'nav','hide user in nav tabs' ) } else { let nav = { 'layout' : layoutId, 'label' : ( gui.pages[ layoutId ].navLabel ? gui.pages[ layoutId ].navLabel : gui.pages[ layoutId ].title ) } if ( gui.pages[ layoutId ].navId ) { nav.id = gui.pages[ layoutId ].navId } else { nav.id = layoutId.replace(/[\W_]+/g, '' ) } if ( gui.pages[ layoutId ].navHTML ) { nav.html = gui.pages[ layoutId ].navHTML } if ( gui.pages[ layoutId ].info ) { nav.info = gui.pages[ layoutId ].info navTabs[ subMenus[ subMenu ] ].info = '!' //log.info('nav', navTabs ) } navTabs[ subMenus[ subMenu ] ].menuItems.push( nav ) } } } } } if ( gui.dynamicNavCallback ) { // generate nav on the fly via callback try { let newNavTabs = await gui.dynamicNavCallback( 'nav', navTabs, req ) if ( newNavTabs ) { navTabs = newNavTabs } } catch( exc ) { log.warn( 'easy-web-app /svc/nav dynamicNavCallback', exc ) } } // show menu only, if it's more than one page if ( navTabs.length == 1 && ! Object.keys( gui.pullDownMenu ).length > 0 ) navTabs = [] let menuCfg = { 'navigations' : navTabs } if ( gui.navSubMenu ) { menuCfg.subMenuConfiguration = gui.navSubMenu } res.json( menuCfg ) } catch ( exc ) { log.warn( 'easy-web-app /svc/nav', exc ) res.json( { 'navigations' : [] } ) } } ); // ---------------------------------------------------------------------------- // i18n: /** first call switches on i18n, further calls add supported languages */ gui.addLang = function addLang( langCode, translations ) { var aTranslation = {} if ( translations ) { aTranslation = translations } if ( ! this.lang ) { this.lang = { } this.lang[ langCode ] = aTranslation this.pages[ 'main' ].header.modules.push( { 'id': 'LangSel', 'type': 'i18n' ,'param': { 'langList': [ langCode ] } } ) } else { this.lang[ langCode ] = aTranslation for (var i = 0; i < this.pages[ 'main' ].header.modules.length; i++) { var mod = this.pages[ 'main' ].header.modules[i] if ( mod.id == 'LangSel' ) { mod.param.langList.push( langCode ) } } } } gui.addTranslation = function addTranslation( langCode, label, translation ) { if ( gui.lang && gui.lang[ langCode ] ) { gui.lang[ langCode ][label] = translation } } router.get( '/i18n/:lang', async function( req, res ) { if ( req.params.lang ) { var langCode = req.params.lang.substring( 0, 2 ) if ( gui.getTranslation ) { var translation = await gui.getTranslation( req, langCode ) res.json( translation ) } else if ( gui.lang && gui.lang[ langCode ] ) { res.json( gui.lang[ langCode ] ) } else { res.json( {} ) // don't offer any translations } } else { res.json( {} ) // don't offer any translations } } ) // ---------------------------------------------------------------------------- /** High level API to add a IO view */ gui.addIoView = function addIoView( page ) { if ( !this.io ) { // first initialization this.io = []; /** REST web service to GET IO data */ router.get ( '/svc/io/:ioId', function ( req, res ) { // console.log( 'ping: ' + req.params.ioId ); res.json( gui.io[ req.params.ioId ] ); } ); router.post( '/svc/io/:ioId', jsonParser, function ( req, res ) { // console.log( JSON.stringify( req.body ) ); if ( req.body && req.body.id && req.params.ioId ) { var ioID = req.params.ioId; var ctlID = req.body.id; if ( gui.io[ ioID ][ ctlID ] ) { if ( req.body.value ) { gui.io[ ioID ][ ctlID ].value = req.body.value; } if ( gui.io[ ioID ][ ctlID ].callBack ) { gui.io[ ioID ][ ctlID ].callBack ( req.body.value , ctlID ) } else { log.warn( 'POST /svc/io', 'CallBack undefined: "' + ctlID + '"' ) } } else { log.warn( 'POST /svc/io', 'Not defined: gui.io[ '+ioID+' ][ '+ctlID+' ]' ) } } else { log.warn( 'POST /svc/io', 'Not valid: req.body && req.body.id && req.params.ioId' ) } res.statusCode = 200; return res.json( gui.io[ req.params.ioId ] ); } ); } var ioId = this.io.length; this.io.push( {} ); // define IO Object var io = gui.addView( { 'id' : 'io' + ioId, 'type' : 'pong-io' }, { 'imgURL' : 'img.png', 'dataURL' : ( gui.appRoot=='/' ? '' : gui.appRoot ) + '/svc/io/' + ioId, 'poll' : '10000', 'io' : [] }, page ) io.ioId = ioId; // IO method to define update polling io.setUpdateMilliSec = function (ms ) { this.moduleConfig.poll = ms; } // IO method to define update polling io.setBackgroundImage = function ( imgFullPath ) { if ( imgFullPath ) { var bgImgName = ( gui.appRoot=='/' ? '' : gui.appRoot ) + '/local' + imgFullPath.substring( imgFullPath.lastIndexOf( '/' ) ); var bgImgPath = imgFullPath.substring( 0, imgFullPath.lastIndexOf( '/' ) ); // log.info( 'static /local', bgImgPath +' '+ bgImgName); this.moduleConfig.imgURL = bgImgName; router.use ( '/local', express.static( bgImgPath ) ); } else { this.moduleConfig.imgURL = null } } io.addLED = function ( id, x, y, value ) { if ( id && (x >= 0) && (y >= 0) ) { this.moduleConfig.io.push ( { id : id, type : "LED", pos : { "x" : x, "y" : y } } ) gui.io[ this.ioId ][ id ] = { value : (value || 0) } } else { log.warn( 'addLED', 'need proper "ID", "x" and "y" values!' ); } } io.setLED = function( id, value ) { if ( id && gui.io[ this.ioId ][ id ] ) { if ( [ -1, 0, 1 ].indexOf ( value ) >= 0 ) { gui.io[ this.ioId ][ id ].value = value; // console.log( value ); } else { log.warn( 'setLED', 'value "' + value + '" ignored' ); } } else { log.warn( 'setLED',':id "' + io + '" unknown' ); } } io.addSwitch = function( id, x, y, values, callBack ) { if ( values && values.length > 0 ) { this.moduleConfig.io.push ( { id : id, type : "Switch", pos : { x : x, y : y }, values : values } ); gui.io[ this.ioId ][ id ] = { value : values[ 0 ], callBack : callBack }; } else { log.warn( 'addSwitch: ', 'Ignored, values should be an array!' ); } } io.addStaticLabel = function( text, x, y ) { var label = null if ( text && x && y ) { label = { id:'label'+(lId++), type:'Label', label:text, pos: { x:''+x, y:''+y } } this.moduleConfig.io.push( label ) } return label } // add general config obj io.addIoElementConfig = function( config, callback ) { if ( config ) { this.moduleConfig.io.push ( config ) if ( callback && config.id ) { gui.io[ this.ioId ][ config.id ] = { value : null, callBack : callback } } } } return io } var lId = 0 // ---------------------------------------------------------------------------- // security /** add pong-security plug in to header */ gui.enableSecurity = function enableBasicAuth( paramObj ) { if ( gui.sec2Params ) { delete gui.sec2Params } gui.secParams = {} if ( ! gui.loginTimeout ) { gui.loginTimeout = 6400000 } var root = ( this.appRoot== '/' ? '' : this.appRoot ) if ( ! paramObj ) {paramObj = {} } gui.secParams.divLayout = ( paramObj.divLayout ? true : false ) gui.secParams.loginURL = ( paramObj.loginURL ? paramObj.loginURL : root+'/login' ) if ( paramObj.resetPasswordURL ) gui.secParams.resetPasswordURL = paramObj.resetPasswordURL gui.secParams.loginPage = ( paramObj.loginPage ? paramObj.loginPage : 'main' ) if ( paramObj.registgerURL ) gui.secParams.registgerURL = paramObj.registgerURL if ( paramObj.registgerURL ) gui.secParams.registerURL = paramObj.registerURL gui.secParams.logoutPage = ( paramObj.logoutPage ? paramObj.logoutPage : 'main' ) gui.secParams.logoutURL = ( paramObj.logoutURL ? paramObj.logoutURL : root+'/logout' ) gui.secParams.sessionExpiredAlert = ( paramObj.sessionExpiredAlert ? paramObj.sessionExpiredAlert : false ) gui.secParams.changePasswordStrength = 4 this.pages[ 'main' ].header.modules.push( { 'id': 'Sec', 'type': 'pong-security', 'param': gui.secParams } ) // basic login impl calls authenticate router.post( '/login', formParser, async function(req, res) { try{ // log.info( "POST Login ..." ) if ( gui.authenticate != null ) { if ( req.body && req.body.userid ) { // log.info( "calling authenticate ..." ) gui.authenticate( req.body.userid, req.body.password, async function ( err, loginOK, mustChangePassword ) { if ( !err && loginOK ) { // log.info( "Login OK" ) res.statusCode = 200 // todo set up "session" for user via hook var token = '' if ( gui.createToken ) { token = await gui.createToken( req.body.userid ) } else { token = gui.mkToken( 32 ) } // log.info( "Token: "+token ) gui.userTokens[ token ] = { userId : req.body.userid, expires: new Date( Date.now() + gui.loginTimeout ), csrfToken: gui.mkToken( 20 ) } res.cookie( 'pong-security', token, { //expires: new Date(Date.now() + 6400000), httpOnly: true, path: gui.appRoot } ) // console.log( 'this.appRoot='+gui.appRoot ) if ( gui.changePassword ) { gui.secParams.changePasswordURL = ( gui.appRoot=='/' ? '/password' : gui.appRoot+'/password' ) } var changePassword = false if ( mustChangePassword ) { changePassword = true } res.json( { loginResult: "Login OK", changePassword: changePassword } ) } else { // log.info( "Login failed!" ) res.status( 401 ).send( "Login failed" ) } } ) } else { var reqUser = await gui.getUserNameFromReq( req ) // just to display in GUI if ( reqUser ) { res.status( 200 ).send( reqUser ) return } else { // log.info( "user/password or cookie required" ) res.status( 401 ).send( "Login failed" ) return } } } else { // log.info( "Please implement authenticate function:" ) // log.info( " gui.authenticate = function authenticate(user, // password)"+ // " { ... return true/false }") res.status( 401 ).send( "Login failed" ) } } catch ( exc ) { log.warn( 'easy-web-app /login', exc ) res.status( 500 ).send( "Login failed" ) } }) // Change Password ReST Service router.post( '/password', formParser, async function(req, res) { // log.info( "POST Login ..." ) var userId = await gui.getUserIdFromReq( req ) if ( userId ) { if ( gui.changePassword != null ) { if ( req.body && req.body.oldPassword && req.body.newPassword ) { var oldPwd = req.body.oldPassword var newPwd = req.body.newPassword gui.changePassword( userId, oldPwd, newPwd, function( err, result ) { //log.info( "callback", "err:"+err+" result:"+result ) if ( result ) { res.status( 200 ).send( "Password changed!" ) } else { res.status( 400 ).send( "Failed to change password!" ) } } ) } else { res.status( 400 ).send( "Invalid request!" ) } // TODO } else { res.status( 405 ).send( "Failed to change password!" ) } } else { res.status( 401 ).send( "Login invalid!" ) } }) // logout ReST service router.post( '/logout', formParser, function(req, res) { // log.info( "POST Logout ..." ) // log.info( 'Cookies: ', req.cookies ) if ( req.cookies && req.cookies[ 'pong-security' ] ) { // log.info( "pong-security cookie ..." ) var token = req.cookies[ 'pong-security' ] // log.info( "token = "+token ) // log.info( "user = " + gui.userTokens[ token ] ) if ( gui.deleteUserIdForToken ) { gui.deleteUserIdForToken( token ) } else if ( gui.userTokens[ token ] ) { delete gui.userTokens[ token ] } } if ( req.session ) { req.session.destroy() } res.clearCookie( 'pong-security', { path: gui.appRoot } ) .status( 200 ).send( "" ) }) log.info( "Security is enabled!" ) } gui.checkUserCSRFtoken = function checkUserCSRFtoken( req ) { if ( req.cookies && req.cookies[ 'pong-security' ] ) { var token = req.cookies[ 'pong-security' ] if ( gui.userTokens[ token ] && gui.userTokens[ token ].csrfToken ) { var headerToken = req.get('X-Protect') var userToken = gui.userTokens[ token ].csrfToken if ( userToken != headerToken ) { return false } } } return true } webservices.use( // inject CSRF token async function( req, res, next ) { try { var csrfToken = 'default' if ( req.cookies && req.cookies[ 'pong-security' ] ) { var token = req.cookies[ 'pong-security' ] if ( gui.getCsrfTokenForUser ) { csrfToken = await gui.getCsrfTokenForUser( token ) } else if ( gui.userTokens[ token ] && gui.userTokens[ token ].csrfToken ) { csrfToken = gui.userTokens[ token ].csrfToken } } } catch ( exc ) { log.warn('easy-web-app CSRF token, exc') } res.header( 'X-Protect', csrfToken ); next(); } ) gui.mkToken = function mkToken( len ) { var charMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" var token ='' for ( var i = 0; i < len; i++ ) { var iRnd = Math.floor( Math.random() * charMap.length ) token += charMap.substring( iRnd, iRnd+1 ) } return token } /** returns null, if not logged in */ gui.getUserIdFromReq = async function getUserIdFromReq( req ) { // console.log( req.headers ); var userId = null // for ( let c in req.cookies ) { // log.info( 'Cookie: '+ c ) // log.info( req.cookies[c] ) // } try { if ( ! req.cookies ) { return null } if ( req.cookies[ 'pongSec2IdTkn' ] ) { // log.info( "UserIdFromReq: pongSec2JWT cookie ..." ) var tokenStr = req.cookies[ 'pongSec2IdTkn' ] var token = jwt.decode( tokenStr, { complete: true }) || {} if ( token.payload && token.payload.name && token.payload.name !== '' ) { userId = token.payload.name } else if ( token.payload && token.payload.email ) { userId = token.payload.email } } else if ( req.cookies[ 'pong-security' ] ) { // log.info( "UserIdFromReq: pong-security cookie ..." ) var token = req.cookies[ 'pong-security' ] // log.info( "UserIdFromReq: token = "+token ) if ( token ) { if ( gui.getUserIdForToken ) { // log.info( "UserIdFromReq..." ) userId = await gui.getUserIdForToken( token ) } else if ( gui.userTokens[ token ] ) { log.verbose( "UserIdFromReq: userId = "+gui.userTokens[ token ].userId ) if ( Date.now() < gui.userTokens[ token ].expires ) { userId = gui.userTokens[ token ].userId } else { log.verbose( "UserIdFromReq: userId = "+gui.userTokens[ token ].userId+" >>>> session expired" ) } } } } else if ( req.headers && req.headers.authorization ) { // log.info( "UserIdFromReq: authorization header ..." ) // try to parse JWT token var parts = req.headers.authorization.split( ' ' ) if ( parts.length == 2 && parts[0] == 'Bearer' ) { var tokenStr = parts[1] var token = jwt.decode( tokenStr, { complete: true }) || {} if ( token.payload && token.payload.name ) { userId = token.payload.name } } } } catch ( exc ) { log.error( 'easy-web-app getUserIdFromReq', exc ) userId = null } return userId } gui.getUserNameFromReq = async function getUserNameFromReq( req ) { var userName = null try { if ( req.cookies && req.cookies[ 'pong-security' ] ) { var token = req.cookies[ 'pong-security' ] if ( token ) { if ( gui.getUserNameForToken ) { userName = await gui.getUserNameForToken( token ) } else { userName = gui.getUserIdFromReq( req ) } } } } catch ( exc ) { log.error( 'easy-web-app getUserIdFromReq', exc ) userName = null } return userName } /** please use getUserIdFromReq instead */ gui.getUserId = function getUserId( req ) { WARNING_DEPRECATED_UserId_FUNCTION() return gui.getLoggedInUserId( req ) } gui.getLoggedInUserId = function getLoggedInUserId( req ) { WARNING_DEPRECATED_UserId_FUNCTION() var uid = null if ( req.cookies && req.cookies[ 'pong-security' ] ) { var token = req.cookies[ 'pong-security' ] if ( gui.userTokens[ token ] ) { if ( Date.now() < gui.userTokens[ token ].expires ) { uid = gui.userTokens[ token ].userId } } } return uid } var showUserIdDeprecated = true function WARNING_DEPRECATED_UserId_FUNCTION() { if ( showUserIdDeprecated ) { showUserIdDeprecated = false // show only once log.warn( 'DEPRECATED: gui.getUserId(req)', 'Use "await getUserIdFromReq()" instead' ) } } // ============================================================================ // sec2 - OpenID Connect gui.enableSec2 = function enableSec2( paramObj ) { if ( gui.secParams ) { delete gui.secParams } gui.sec2Params = paramObj this.pages[ 'main' ].header.modules.push( { 'id': 'Sec2', 'type': 'pong-security2', 'param': gui.sec2Params } ) log.info( "Security v2 is enabled!" ) }