added security & streamlined processing @TODO: add restrictions

v2.4
Steve Nyemba 6 days ago
parent dcd3c0d804
commit b83af618c5

@ -175,7 +175,8 @@ def secure(
""" """
print (_msg) print (_msg)
# def add_login(manifest:Annotated[str,typer.Argument(help="path of the manifest file")],) :
# pass
def load(**_args): def load(**_args):
""" """
This function will load external module form a given location and return a pointer to a function in a given module This function will load external module form a given location and return a pointer to a function in a given module

@ -9,7 +9,7 @@ import json
from . import sites from . import sites
from . import apexchart from . import apexchart
from . import meta from . import meta
from . import secure
class Plugin : class Plugin :
# #
# decorator for plugin functions, this is a preliminary to enable future developement # decorator for plugin functions, this is a preliminary to enable future developement
@ -29,45 +29,6 @@ class Plugin :
setattr(wrapper,'method',self._method) setattr(wrapper,'method',self._method)
setattr(wrapper,'mimetype',self._mimetype) setattr(wrapper,'mimetype',self._mimetype)
return wrapper return wrapper
# @staticmethod
# def load(_path,_filename,_name) :
# """
# This function will load external module form a given location and return a pointer to a function in a given module
# :path absolute path of the file (considered plugin) to be loaded
# :name name of the function to be applied
# """
# # _path = _args['path'] #os.sep.join([_args['root'],'plugin'])
# if os.path.isdir(_path):
# files = os.listdir(_path)
# if files :
# files = [name for name in files if name.endswith('.py') and name == _filename]
# if files:
# _path = os.sep.join([_path,files[0]])
# else:
# return None
# else:
# return None
# #-- We have a file ...
# # _name = _args['name']
# spec = importlib.util.spec_from_file_location(_filename, _path)
# module = importlib.util.module_from_spec(spec)
# spec.loader.exec_module(module)
# #
# # we need to make sure we have the plugin decorator here to make sure
# return getattr(module,_name) if hasattr(module,_name) else None
# @staticmethod
# def stats (_config) :
# """
# Returns the statistics of the plugins
# """
# _data = []
# for _name in _config :
# _log = {"files":_name,"loaded":len(_config[_name]),"logs":json.dumps(_config[_name])}
# _data.append(_log)
# return pd.DataFrame(_data)
@staticmethod @staticmethod
def call(**_args): def call(**_args):
@ -124,66 +85,63 @@ class Plugin :
return _data,_code,{'Content-Type':_mimeType} return _data,_code,{'Content-Type':_mimeType}
pass pass
class delegate:
def __call__(self,**_args) :
_handler= _args['handler'] #-- module handler
_request= _args['request']
_uri = _args['uri'] #_request.path[1:]
# #
# we need to update the _uri (if context/embeded apps) # default plugins to load into the configuration file
@Plugin(mimetype="application/json")
def authorizationURL (**_args):
# _config = _args['config']
_source = _args['config']['system']['source']
# #
# _context = _handler.system()['context'] #
_context = _handler.get('system.context') if 'secure' in _source :
if _context : _path = _source['secure']['path']
_uri = f'{_context}/{_uri}' f = open(_path)
# print ([' ** ',_uri, _args['uri']]) _config = json.loads(f.read())
# _plugins = _handler.plugins() if _config['method'] in ['oauth2','oauth20','oauth2.0'] :
_plugins = _handler.get('plugins') _url = [f"{_key}={_config[_key]}" for _key in _config if _key not in ['method','authorization_url']]
_code = 200
if _uri in _plugins :
# _config = _handler.config() #_args['config']
_config = _handler.get(None) #_args['config']
# return _url
_url = _config['authorization_url']+'?'+'&'.join(_url)
return {"url":_url,"label":f"Login with {_source['secure']['provider']}"}
else:
return {}
_pointer = _plugins[_uri] @Plugin(mimetype="text/html")
_data = {} def oauthFinalize (**_args):
if hasattr(_pointer,'mimetype') and hasattr(_pointer,'method'): _request = _args['request']
#
# we constraint the methods given their execution ...
_mimeType = _pointer.mimetype
if _request.method in _pointer.method :
_data = _pointer(request=_request,config=_config)
elif not hasattr(_pointer,'mimetype'):
_data,_mimeType = _pointer(request=_request,config=_config) _html = """
<script src="/static/js/fontawesome/js/all.js"></script>
else: <script src="/static/js/jx/rpc.js"></script>
_mimeType = 'application/octet-stream'
# _data = _pointer(request=_request,config=_config)
if type(_data) == pd.DataFrame :
_data = _data.to_json(orient='records')
elif type(_data) in [dict,list] :
_data = json.dumps(_data)
pass
else:
_code = 404
#
# We should generate a 500 error in this case with a message ...
#
_mimeType = 'plain/html'
_data = f"""
<script> <script>
// var _args = urlparser(window.location.search)
// I should if (_args.code){
_msg = '<div class="border-round" style="display:grid; grid-template-columns:100px auto; gap:8px; align-content:center"><div class="large-text">404</div>,div> Resource NOT found</di><div>' var http = HttpClient.instance()
var _uri = '/login?code='+_args.code
http.post(_uri,(x)=>{
if(x.status == 200){
sessionStorage.redirect = x.responseURL
window.open(x.responseURL,'_self')
console.log(x.responseURL)
window.close()
}else{
}
})
}
</script> </script>
<div align="center" style="margin-top:35%; justify-items:center">
<i class="fa-solid fa-cog fa-spin fa-5x" style="color:#CAD5E0"></i>
<i class="fa-solid fa-cog fa-spin fa-2x" style="color:#ff6500"></i>
<div style="font-size:13px">Please wait ...</div>
</div>
""" """
# if _mimeType in ['text/html','plain/html'] : return _html
# _env = Environment(loader=BaseLoader()).from_string(_data)
# _kwargs = {'layout':_config['layout'],'system':_config['system']}
# print (_data)
# _data = _env.render(**_kwargs)
return _data,_code,{'Content-Type':_mimeType}

@ -80,39 +80,40 @@ def _realpath (uri,_config) :
return _uri return _uri
def _format (uri,_config): # def _format (uri,_config):
_layout = _config['layout'] # _layout = _config['layout']
if 'location' in _layout : # if 'location' in _layout :
return 'api/disk/read?uri='+uri # return 'api/disk/read?uri='+uri
return uri # return uri
def read (**_args): # def read (**_args):
""" # """
This will read binary files from disk, and allow the location or not to be read # This will read binary files from disk, and allow the location or not to be read
@TODO: add permissions otherwise there can be disk-wide reads # @TODO: add permissions otherwise there can be disk-wide reads
""" # """
request = _args['request'] if 'request' in _args else None # request = _args['request'] if 'request' in _args else None
_layout = _args['config']['layout'] # _layout = _args['config']['layout']
_uri = request.args['uri'] if request else _args['uri'] # if 'location' in _layout : # _uri = request.args['uri'] if request else _args['uri'] # if 'location' in _layout :
# _uri = os.sep.join([_layout['location'],_uri]) # # _uri = os.sep.join([_layout['location'],_uri])
_uri = _realpath(_uri, _args['config']) # _uri = _realpath(_uri, _args['config'])
_mimeType = 'text/plain' # _mimeType = 'text/plain'
# _stream = None
if os.path.exists(_uri): # if os.path.exists(_uri):
f = open(_uri,mode='rb') # f = open(_uri,mode='rb')
_stream = f.read() # _stream = f.read()
f.close() # f.close()
# # #
# Inferring the type of the data to be returned # # Inferring the type of the data to be returned
_mimeType = 'application/octet-stream' # _mimeType = 'application/octet-stream'
_extension = _uri.split('.')[-1] # _extension = _uri.split('.')[-1]
if _extension in ['css','js','csv','html'] : # if _extension in ['css','js','csv','html'] :
_mimeType = f'text/{_extension}' # _mimeType = f'text/{_extension}'
if _extension == 'js' : # if _extension == 'js' :
_mimeType = 'text/javascript' # _mimeType = 'text/javascript'
elif _extension in ['png','jpg','jpeg'] : # elif _extension in ['png','jpg','jpeg'] :
_mimeType = f'image/{_extension}' # _mimeType = f'image/{_extension}'
return _stream, _mimeType
return None,_mimeType # return _stream, _mimeType
# return None,_mimeType
def exists(**_args): def exists(**_args):
_path = _realpath(_args['uri'],_args['config']) _path = _realpath(_args['uri'],_args['config'])
@ -133,12 +134,15 @@ def html(_uri,_config) :
_layout = _config['layout'] _layout = _config['layout']
if 'location' in _layout : if 'location' in _layout :
if not _config : if not _config :
_api = os.sep.join(['api/disk/read?uri=',copy.copy(_layout['root']) ]) # _api = os.sep.join(['api/disk/read?uri=',copy.copy(_layout['root']) ])
_api = '/'.join(['files',copy.copy(_layout['root'])])
else: else:
_api = os.sep.join([f'{_context}/api/disk/read?uri=',copy.copy(_layout['root'])]) # _api = os.sep.join([f'{_context}/api/disk/read?uri=',copy.copy(_layout['root'])])
_api = '/'.join([f'{_context}/files',copy.copy(_layout['root'])])
if f"{_layout['root']}" in _html : if f"{_layout['root']}" in _html :
# _html = _html.replace('/api/disk/read?uri=','').replace(f"{_layout['root']}",_api)
_html = _html.replace('/api/disk/read?uri=','').replace(f"{_layout['root']}",_api) _html = _html.replace('/api/disk/read?uri=','').replace(f"{_layout['root']}",_api)
_html = mistune.html(_html).replace("&quot;",'"').replace("&lt;","<").replace("&gt;",">") if _uri[-2:] in ['md','MD','Md','mD'] else _html _html = mistune.html(_html).replace("&quot;",'"').replace("&lt;","<").replace("&gt;",">") if _uri[-2:] in ['md','MD','Md','mD'] else _html
# _html = _html.replace(f'{os.sep}{_layout["root"]}',_layout['root']) # _html = _html.replace(f'{os.sep}{_layout["root"]}',_layout['root'])
@ -154,10 +158,11 @@ def plugins (**_args):
""" """
_context = _args['context'] _context = _args['context']
if 'path' not in _args : if 'path' not in _args :
key = 'api/disk/read' # key = 'api/disk/read'
if _context : # if _context :
key = f'{_context}/{key}' # key = f'{_context}/{key}'
return {key:read} # return {key:read}
return None
_path = _args['path'] #os.sep.join([_args['root'],'plugin']) _path = _args['path'] #os.sep.join([_args['root'],'plugin'])
loader = plugin_ix.Loader(file=_path) loader = plugin_ix.Loader(file=_path)

@ -9,377 +9,3 @@
# import importlib # import importlib
# import importlib.util # import importlib.util
from cms import disk, cloud from cms import disk, cloud
# from . import basic
# class Loader :
# """
# This class is designed to exclusively load configuration from disk into an object
# :path path to the configuraiton file
# :location original location (caller)
# """
# def __init__(self,**_args):
# self._path = _args['path']
# self._original_location = None if 'location' not in _args else _args['location']
# self._location = None
# self._caller = None if 'caller' not in _args else _args['caller']
# print ([' *** ', self._caller])
# self._menu = {}
# self._plugins={}
# self.load()
# def load(self):
# """
# This function will load menu (overwrite) and plugins
# """
# self.init_config()
# self.init_menu()
# self.init_plugins()
# def init_config(self):
# """
# Initialize & loading configuration from disk
# """
# f = open (self._path)
# self._config = json.loads(f.read())
# if self._caller :
# self._location = self._original_location.split(os.sep) # needed for plugin loading
# self._location = os.sep.join(self._location[:-1])
# self._config['system']['portal'] = self._caller != None
# #
# # let's see if we have a location for a key (i.e security key) in the configuration
# #
# self.update_config()
# # _system = self._config['system']
# # if 'source' in _system and 'key' in _system['source'] :
# # _path = _system['source']['key']
# # if os.path.exists(_path):
# # f = open(_path)
# # _system['source']['key'] = f.read()
# # f.close()
# # self._system = _system
# # self._config['system'] = _system
# def update_config(self):
# """
# We are going to update the configuration (source.key, layout.root)
# """
# _system = self._config['system']
# #
# # updating security-key that allows the application to update on-demand
# if 'source' in _system and 'key' in _system['source'] :
# _path = _system['source']['key']
# if os.path.exists(_path):
# f = open(_path)
# _system['source']['key'] = f.read()
# f.close()
# self._system = _system
# self._config['system'] = _system
# _layout = self._config['layout']
# #
# # update root so that the app can be launched from anywhere
# # This would help reduce the footprint of the app/framework
# _path = os.sep.join(self._path.split(os.sep)[:-1])
# _p = 'source' not in _system
# _q = 'source' in _system and _system['source']['id'] != 'cloud'
# _r = os.path.exists(_layout['root'])
# if not _r and (_p or _q) :
# #
# # If we did running this app from installed framework (this should not apply to dependent apps)
# #
# _root = os.sep.join([_path,_layout['root']])
# self._config['layout']['root'] = _root
# self._config['layout']['root_prefix'] = _root
# def init_menu(self):
# """
# This function will read menu and sub-menu items from disk structure,
# The files are loaded will
# """
# _config = self._config
# if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' :
# _sourceHandler = cloud
# else:
# _sourceHandler = disk
# _object = _sourceHandler.build(_config)
# #
# # After building the site's menu, let us add the one from 3rd party apps
# #
# _layout = copy.deepcopy(_config['layout'])
# _overwrite = _layout['overwrite'] if 'overwrite' in _layout else {}
# #
# # @TODO: Find a way to translate rename/replace keys of the _object (menu) here
# #
# #-- applying overwrites to the menu items
# for _name in _object :
# _submenu = _object[_name]
# _index = 0
# for _item in _submenu :
# text = _item['text'].strip()
# if text in _overwrite :
# if 'uri' in _item and 'url' in 'url' in _overwrite[text] :
# del _item['uri']
# _item = dict(_item,**_overwrite[text])
# if 'uri' in _item and 'type' in _item and _item['type'] != 'open':
# _item['uri'] = _item['uri'].replace(_layout['root'],'')
# _submenu[_index] = _item
# _index += 1
# self.init_apps(_object)
# self._menu = _object
# self._order()
# def init_apps (self,_menu):
# """
# Insuring that the apps are loaded into the menu with an approriate label
# """
# _system = self._config['system']
# _context = _system['context']
# if 'routes' in _system :
# # _items = []
# _overwrite = {} if 'overwrite' not in self._config['layout'] else self._config['layout']['overwrite']
# for _text in _system['routes'] :
# _item = _system['routes'][_text]
# if 'menu' not in _item :
# continue
# uri = f'{_context}/{_text}'
# # _items.append ({"text":_text,'uri':uri,'type':'open'})
# _label = _item['menu']
# if _label not in _menu :
# _menu [_label] = []
# _menu[_label].append ({"text":_text,'uri':uri,'type':'open'})
# # _overwrite[_text] = {'text': _text.replace('-',' ').replace('_',' '),'uri':uri,'type':'open'}
# # _menu['products'] = _items
# #
# # given that the menu items assumes redirecting to a page ...
# # This is not the case
# #
# # self._config['overwrite'] = _overwrite
# else:
# pass
# pass
# def _order (self):
# _config = self._config
# if 'order' in _config['layout'] and 'menu' in _config['layout']['order']:
# _sortedmenu = {}
# _menu = self._menu
# for _name in _config['layout']['order']['menu'] :
# if _name in _menu :
# _sortedmenu[_name] = _menu[_name]
# _menu = _sortedmenu if _sortedmenu else _menu
# #
# # If there are missing items in the sorting
# _missing = list(set(self._menu.keys()) - set(_sortedmenu))
# if _missing :
# for _name in _missing :
# _menu[_name] = self._menu[_name]
# _config['layout']['menu'] = _menu #cms.components.menu(_config)
# self._menu = _menu
# self._config = _config
# def init_plugins(self) :
# """
# This function looks for plugins in the folder on disk (no cloud support) and attempts to load them
# """
# _config = self._config
# PATH= os.sep.join([_config['layout']['root'],'_plugins'])
# if not os.path.exists(PATH) :
# #
# # we need to determin if there's an existing
# PATH = os.sep.join(self._path.split(os.sep)[:-1]+ [PATH] )
# if not os.path.exists(PATH) and self._location and os.path.exists(self._location) :
# #
# # overriding the location of plugins ...
# PATH = os.sep.join([self._location, _config['layout']['root'],'_plugins'])
# _map = {}
# # if not os.path.exists(PATH) :
# # return _map
# if 'plugins' not in _config :
# _config['plugins'] = {}
# _conf = _config['plugins']
# for _key in _conf :
# _path = os.sep.join([PATH,_key+".py"])
# if not os.path.exists(_path):
# continue
# for _name in _conf[_key] :
# _pointer = self._load_plugin(path=_path,name=_name)
# if _pointer :
# _uri = "/".join(["api",_key,_name])
# _map[_uri] = _pointer
# #
# # We are adding some source specific plugins to the user-defined plugins
# # This is intended to have out-of the box plugins...
# #
# if 'source' in _config['system'] and _config['system']['source']['id'] == 'cloud' :
# _plugins = cloud.plugins()
# else:
# _plugins = disk.plugins()
# #
# # If there are any plugins found, we should load them and use them
# if _plugins :
# _map = dict(_map,**_plugins)
# else:
# pass
# self._plugins = _map
# self._config['plugins'] = self._plugins
# def _load_plugin(self,**_args):
# """
# This function will load external module form a given location and return a pointer to a function in a given module
# :path absolute path of the file (considered plugin) to be loaded
# :name name of the function to be applied
# """
# _path = _args['path'] #os.sep.join([_args['root'],'plugin'])
# if os.path.isdir(_path):
# files = os.listdir(_path)
# if files :
# files = [name for name in files if name.endswith('.py')]
# if files:
# _path = os.sep.join([_path,files[0]])
# else:
# return None
# else:
# return None
# #-- We have a file ...
# _name = _args['name']
# spec = importlib.util.spec_from_file_location(_name, _path)
# module = importlib.util.module_from_spec(spec)
# spec.loader.exec_module(module)
# return getattr(module,_name) if hasattr(module,_name) else None
# class Getter (Loader):
# def __init__(self,**_args):
# super().__init__(**_args)
# def load(self):
# super().load()
# _system = self.system()
# _logo = _system['logo']
# if 'source' in _system and 'id' in _system['source'] and (_system['source']['id'] == 'cloud'):
# _icon = f'/api/cloud/download?doc=/{_logo}'
# _system['icon'] = _icon
# else:
# _root = self._config['layout']['root']
# _icon = os.sep.join([_root,_logo])
# _system['icon'] = _logo
# self._config['system'] = _system
# if self._caller :
# _system['caller'] = {'icon': self._caller.system()['icon']}
# def html(self,uri,id,_args={},_system={}) :
# """
# This function reads a given uri and returns the appropriate html document, and applies environment context
# """
# _system = self._config['system']
# if 'source' in _system and _system['source']['id'] == 'cloud':
# _html = cloud.html(uri,dict(_args,**{'system':_system}))
# else:
# _html = disk.html(uri,self.layout())
# # _html = (open(uri)).read()
# #return ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>'])
# _html = ' '.join(['<div id=":id" class=":id">'.replace(':id',id),_html,'</div>'])
# appContext = Environment(loader=BaseLoader()).from_string(_html)
# _args['system'] = _system
# #
# # If the rendering of the HTML happens here we should plugin custom functions (at the very least)
# #
# return appContext.render(**_args)
# # return _html
# def data (self,_args):
# """
# :store data-store parameters (data-transport, github.com/lnyemba/data-transport)
# :query query to be applied against the store (expected data-frame)
# """
# _store = _args['store']
# reader = transport.factory.instance(**_store)
# _queries= copy.deepcopy(_store['query'])
# _data = reader.read(**_queries)
# return _data
# def csv(self,uri) :
# return pd.read(uri).to_html()
# return _map
# def menu(self):
# return self._config['menu']
# def plugins(self):
# return copy.deepcopy(self._plugins) if 'plugins' in self._config else {}
# def context(self):
# """
# adding custom variables functions to Jinja2, this function should be called after plugins are loaded
# """
# _plugins = self.plugins()
# # if not location:
# # env = Environment(loader=BaseLoader())
# # else:
# location = self._config['layout']['root']
# # env = Environment(loader=FileSystemLoader(location))
# env = Environment(loader=BaseLoader())
# # env.globals['routes'] = _config['plugins']
# return env
# def config(self):
# return copy.deepcopy(self._config)
# def system(self,skip=[]):
# """
# :skip keys to ignore in the object ...
# """
# _data = copy.deepcopy(self._config['system'])
# _system = {}
# if skip and _system:
# for key in _data.keys() :
# if key not in skip :
# _system[key] = _data[key]
# else:
# _system= _data
# return _system
# def layout(self):
# return self._config['layout']
# def get_app(self):
# return self._config['system']['app']
# class Router :
# def __init__(self,**_args) :
# # _app = Getter (path = path)
# _app = Getter (**_args)
# self._id = 'main'
# # _app.load()
# self._apps = {}
# _system = _app.system()
# if 'routes' in _system :
# _system = _system['routes']
# for _name in _system :
# _path = _system[_name]['path']
# self._apps[_name] = Getter(path=_path,caller=_app,location=_path)
# self._apps['main'] = _app
# def set(self,_id):
# self._id = _id
# def get(self):
# return self._apps['main'] if self._id not in self._apps else self._apps[self._id]
# def get_main(self):
# return self._apps['main']

@ -5,46 +5,9 @@ import importlib.util
import os import os
# #
# Defining the decorator to be used in plugins, this will enable simple loading and assigning mimetype to the output (if any) # let's define the default plugins that will be included into the
# #
def authorizationURL (**_args):
# def stats (_config) : # _config = _args['config']
# """ _source = _args['config']['system']['source']
# Returns the statistics of the plugins
# """
# _data = []
# for _name in _config :
# _log = {"files":_name,"loaded":len(_config[_name]),"logs":json.dumps(_config[_name])}
# _data.append(_log)
# return pd.DataFrame(_data)
# pass
# def load(_path,_filename,_name) :
# """
# This function will load external module form a given location and return a pointer to a function in a given module
# :path absolute path of the file (considered plugin) to be loaded
# :name name of the function to be applied
# """
# # _path = _args['path'] #os.sep.join([_args['root'],'plugin'])
# if os.path.isdir(_path):
# files = os.listdir(_path)
# if files :
# files = [name for name in files if name.endswith('.py') and name == _filename]
# if files:
# _path = os.sep.join([_path,files[0]])
# else:
# return None
# else:
# return None
# #-- We have a file ...
# # _name = _args['name']
# spec = importlib.util.spec_from_file_location(_filename, _path)
# module = importlib.util.module_from_spec(spec)
# spec.loader.exec_module(module)
# #
# # we need to make sure we have the plugin decorator here to make sure
# return getattr(module,_name) if hasattr(module,_name) else None

@ -1,8 +1,5 @@
__doc__ = """
arguments : from flask import Flask,render_template,make_response,request, redirect, Response, session
--config path of the configuration otherwise it will look for the default in the working directory
"""
from flask import Flask,render_template,send_from_directory,request, redirect, Response, session
import flask import flask
#import transport #import transport
#from transport import providers #from transport import providers
@ -19,7 +16,7 @@ import typer
from typing_extensions import Annotated from typing_extensions import Annotated
from typing import Optional from typing import Optional
import numpy as np
import pandas as pd import pandas as pd
import uuid import uuid
import datetime import datetime
@ -31,27 +28,27 @@ import cms.sites
_app = Flask(__name__) _app = Flask(__name__)
cli = typer.Typer() cli = typer.Typer()
def _getHandler (app_id,resource=None) : # def _getHandler (app_id,resource=None) :
global _qcms # global _qcms
_id = _getId(app_id,resource) # _id = _getId(app_id,resource)
return _qcms._apps[_id] # return _qcms._apps[_id]
def _getId(app_id,app_x): # def _getId(app_id,app_x):
if app_x not in [None,''] : # if app_x not in [None,''] :
_uri = '/'.join([app_id,app_x]) # _uri = '/'.join([app_id,app_x])
return _uri[:-1] if _uri.endswith('/') else _uri # return _uri[:-1] if _uri.endswith('/') else _uri
return app_id # return app_id
def _setHandler (app_id,resource) : # def _setHandler (app_id,resource) :
session['app_id'] = _getId(app_id,resource) # session['app_id'] = _getId(app_id,resource)
@_app.route("/favicon.ico",defaults={'app':None}) @_app.route("/favicon.ico",defaults={'app':None})
@_app.route("/<app>/favicon.ico") @_app.route("/<app>/favicon.ico")
def favicon (app): def favicon (app):
global _qcms global _qcms
_site = _qcms.get (app) _site = _qcms.get (request)
return _read(app,None,_site.get('system.icon')) # return _read(app,None,_site.get('system.icon'))
return return redirect(_site.get('system.icon')[1:]) #_site.read(request,uri=_site.get('system.icon'))
@_app.route("/<_id>/robots.txt") @_app.route("/<_id>/robots.txt")
@_app.route("/robots.txt",defaults={'_id':None}) @_app.route("/robots.txt",defaults={'_id':None})
def robots_txt(_id): def robots_txt(_id):
@ -60,7 +57,7 @@ def robots_txt(_id):
menu options menu options
""" """
global _qcms global _qcms
_site = _qcms.get (_id) _site = _qcms.get (request)
_routes = _site.get('system.routes') _routes = _site.get('system.routes')
_context = _site.get('system.context') _context = _site.get('system.context')
_info = [f''' _info = [f'''
@ -78,84 +75,123 @@ def robots_txt(_id):
# return '\n'.join(_info),200,{'Content-Type':'plain/text'} # return '\n'.join(_info),200,{'Content-Type':'plain/text'}
return Response('\n'.join(_info), mimetype='text/plain') return Response('\n'.join(_info), mimetype='text/plain')
def _getIndex (app_id ,resource=None): @_app.before_request
_handler = _getHandler(app_id,resource) def format():
_layout = _handler.layout()
_status_code = 200
global _qcms global _qcms
_index_page = 'index.html' isroute = _qcms.inspect.isroute(request)
_args = {} isfile = _qcms.inspect.isfile(request)
isapi = _qcms.inspect.isapi(request)
_map = ['/reload','/version','/dialog','/login','/logout']
isendpoint = request.path.split('/')[1] in _map or request.path in _map
# print (request.path.split('/'),' ** ',request.path)
if not isendpoint and not isfile and not isapi and not request.path.endswith('/'):
return redirect(request.path+'/')
try : @_app.route("/login",methods=['GET'],strict_slashes = False)
_uri = os.sep.join([_layout['root'],_layout['index']]) def login ():
_id = _getId(app_id,resource)
_args = _qcms.render(_uri,'index',_id)
except Exception as e:
_status_code = 404
_index_page = '404.html'
print(e)
return render_template(_index_page,**_args),_status_code
@_app.route("/")
def _index ():
# return _getIndex('main')
global _qcms global _qcms
_app = _qcms.get(None) # _secureMethod = request.headers.get('method','pam')
_uri = os.sep.join([_app.get('layout.root'),_app.get('layout.index')])
_html = _qcms.render(_uri,'index')
return render_template('index.html',**_html),200 #render_template('index.html',**_qcms.render(_uri,'index')),200
@_app.route("/<app>/<resource>") #
@_app.route("/<app>",defaults={'resource':None}) # are we using a default html document ?
def _altIndex(app,resource): #
_site = _qcms.get(None)
# _page = f'login/{_site.secure.method()}.html'
_uri = _site.get('layout.login')
# _args= _site.get('system.source.secure')
_args = {}
_args['login'] = {'model':_site.secure.method()}
_args['system'] = _site.get('system')
_args['layout'] = _site.get('layout')
#
# add addition information associated with authentication
#
if 'source' in _args['system']:
del _args['system']['source']
if not _uri :
_uri = f'login/{_site.secure.method()}.html'
_html = render_template(_uri,**_args)
else:
#
# let's place the html content here ...
_html = _site.html(_uri,'login')
#
# we need to see what to _do about the session cookies
#
_args['html'] = _html
return render_template('login/login.html',**_args)
@_app.route("/login",methods=['POST'],strict_slashes = False)
def authenticate () :
#
# we will forward this to the appropriate agent
#
global _qcms global _qcms
_id = _getId(app,resource) _site = _qcms.get()
_site = _qcms.get(_id) _page = session.get('redirect','')
_uri = os.sep.join([_site.get('layout.root'),_site.get('layout.index')])
return render_template('index.html',**_qcms.render(_uri,'index',_id)),200 _uri = f"{_site.get('system.context')}/{_page}".replace("//","/")
@_app.route('/files/<path:file>',defaults={'app':None,'module':None}) if _uri.endswith('/') :
@_app.route('/<app>/files/<path:file>', defaults=[{'module':None}]) _uri = _uri[:-1]
@_app.route('/<app>/<module>/files/<path:file>')
def _read(app,module,file): if _site.secure.allow(request=request,uri=_uri) :
return redirect(_uri)
else:
_good,_args = _site.secure.authenticate(request=request)
_site.log(action='authenticate',input={'method':_site.secure.method(),'out':_good})
if not _good :
uri = request.path
if _good :
_key = list(_good.keys())[0]
response = make_response(redirect(_uri))
response.set_cookie(_key,_good[_key],**_args)
response.headers['Location'] = _uri
return response
else:
#
# error 403 must be returned
#
# response = make_response(redirect(_uri))
# response.status = 403
# uri = request.path
pass
return redirect(request.path),403
@_app.route("/logout",methods=["POST",'GET'],strict_slashes = False)
def logout():
global _qcms global _qcms
_id = _getId(app,module) _site = _qcms.get()
_site = _qcms.get(_id) _id = _site.secure._authContext #-- cookie key
_stream,_mimeType = _site.read(file) resp = make_response(redirect("/"))
resp.delete_cookie(_id)
return resp
return io.BytesIO(_stream),200,{'Content-Type':_mimeType} @_app.route('/dialog',defaults={'app':None},methods=['POST','GET'], strict_slashes = False)
@_app.route('/<app>/dialog') @_app.route('/<path:app>/dialog', methods=['POST','GET'], strict_slashes = False)
@_app.route('/dialog',defaults={'app':None})
def _getdialog(app): def _dialog(app):
global _qcms global _qcms
_site = _qcms.get(app) # _site = _qcms.get(app)
_site = _qcms.get(request)
_uri = request.headers['uri'] _uri = request.headers['uri']
_id = request.headers['dom'] _id = request.headers['dom']
# _html = ''.join(["<div style='padding:1%'>",str( e.render(**_args)),'</div>']) # _html = ''.join(["<div style='padding:1%'>",str( e.render(**_args)),'</div>'])
_args = _qcms.render(_uri,'html',app) #session.get('app_id','main')) # _args = _qcms.render(_uri,'html',app) #session.get('app_id','main'))
_args = _site.render(id='html', request=request)
_args['title'] = _id _args['title'] = _id
return render_template('dialog.html',**_args) #title=_id,html=_html) return render_template('dialog.html',**_args) #title=_id,html=_html)
@_app.route("/reload/<key>",strict_slashes = False)
@_app.route("/api/<module>/<name>",defaults={'app':None,'key':None},methods=['GET','POST','DELETE','PUT'])
@_app.route("/<app>/api/<module>/<name>",defaults={'key':None},methods=['GET','POST','DELETE','PUT'])
@_app.route("/<app>/<key>/api/<module>/<name>",methods=['GET','POST','DELETE','PUT'])
def _proxyCall(app,key,module,name):
global _qcms
_id = _getId(app,key)
_site = _qcms.get(_id)
_uri = f'api/{module}/{name}'
_delegate = cms.delegate()
return _delegate(uri=_uri, handler=_site, request=request)
@_app.route('/version')
def _version ():
global _qcms
_site = _qcms.get()
global _config
return _site.get('system.version')
@_app.route("/reload/<key>")
def _reload(key) : def _reload(key) :
global _qcms global _qcms
@ -167,11 +203,54 @@ def _reload(key) :
else: else:
return "",403 return "",403
@_app.route('/reload',methods=['POST']) @_app.route('/reload/',methods=['POST'],strict_slashes = False)
def reload(): def reload():
_key = request.headers['key'] if 'key' in request.headers else None _key = request.headers['key'] if 'key' in request.headers else None
return _reload(_key) return _reload(_key)
@_app.route("/",defaults={'file':None}, methods=['POST','GET','PUT'], strict_slashes = False)
@_app.route('/<path:file>',defaults={}, methods=['POST','GET','PUT'], strict_slashes = False)
# @_app.route('/<app>/files/<path:file>', defaults=[{'module':None}])
# @_app.route('/<app>/<module>/files/<path:file>')
def _read(file):
global _qcms
#
# apply security here (on the top level site only)
# @TODO: upon failure, need to forward this to an error page ...
#
_site = _qcms.get(None)
if _site.secure.allow(request=request) :
return _qcms.delegate(request)
else:
session['redirect'] = request.path
return redirect("/login")
# @_app.route("/api/<path:uri>", methods=['POST','GET','PUT'], defaults={'uri':None,'route':None})
# @_app.route("<path:route>/api/<path:uri>", methods=['POST','GET','PUT'])
# def _api (route,uri) :
# # print ()
# pass
# @_app.route("/api/<module>/<name>",defaults={'app':None,'key':None},methods=['GET','POST','DELETE','PUT'])
# @_app.route("/<app>/api/<module>/<name>",defaults={'key':None},methods=['GET','POST','DELETE','PUT'])
# @_app.route("/<app>/<key>/api/<module>/<name>",methods=['GET','POST','DELETE','PUT'])
# def _proxyCall(app,key,module,name):
# global _qcms
# _id = _getId(app,key)
# _site = _qcms.get(request)
# _uri = f'api/{module}/{name}'
# _delegate = cms.delegate()
# return _delegate(uri=_uri, handler=_site, request=request)
@_app.route('/version',strict_slashes = False)
def _version ():
global _qcms
_site = _qcms.get()
global _config
return _site.get('system.version')
@_app.route('/page',methods=['POST'],defaults={'app_id':None,'key':None}) @_app.route('/page',methods=['POST'],defaults={'app_id':None,'key':None})
@_app.route('/<app_id>/page',methods=['POST'],defaults={'key':None}) @_app.route('/<app_id>/page',methods=['POST'],defaults={'key':None})
@ -244,3 +323,4 @@ def _help() :
pass pass
if __name__ == '__main__' : if __name__ == '__main__' :
cli() cli()

@ -1,5 +1,5 @@
__author__ = "Steve L. Nyemba" __author__ = "Steve L. Nyemba"
__version__= "2.4.0" __version__= "2.4.2"
__email__ = "steve@the-phi.com" __email__ = "steve@the-phi.com"
__license__=""" __license__="""
Copyright 2010 - 2024, Steve L. Nyemba, Vanderbilt University Medical Center Copyright 2010 - 2024, Steve L. Nyemba, Vanderbilt University Medical Center

@ -0,0 +1,174 @@
import os
import json
import transport
import plugin_ix
import copy
import uuid
import pandas as pd
import io
class Manager :
"""
{secure:{path,registry}}
qcms secure <project> will secure the project (download authentication modules)
"""
def __init__(self,**_args):
#
# let's find the security configuration file
#
_appConfig = _args['config']
self._config = None
self._regPath = None
self._authKey = None
self._authContext = None
_appContext = _args['config']['system']['context']
self._loginURI = None
_csv = """user,uri,allow\n*,*,1"""
self._permissions = pd.read_csv(io.StringIO(_csv))
if 'secure' in _args['config']['system']['source'] :
self._authContext = _appConfig['system']['source']['secure']['id']
self._path = _appConfig['system']['source']['secure']['path']
self.inspect(self._path,'authentication configuration')
if os.path.exists (self._path ) :
_content = (open(self._path )).read()
self._config = json.loads(_content)
self._authKey= self._config['method'] #-- pam, oauth2, nextcloud, ssh
#
# the configuration {method,token}
#
# plugins home folder
_path = _appConfig['system']['source']['secure']['registry']
self.inspect(_path,'plugins installed')
self._plugins = plugin_ix.Registry(folder=_path)
self._plugins.load()
#
# we need to load the permissions here ...
#
_kwargs = _appConfig['system']['source']['secure']
self._loginURI = [_appConfig['system']['context']]
if 'uri' in _kwargs :
self._loginURI.appendd(_kwargs['uri'])
else:
self._loginURI.append('login')
# if 'authorization' not in _kwargs:
#
# we will assume that all authenticated users have access to every part of the site
#
if 'authorization' in _kwargs:
#
# loading permissions table from a designated location
reader = transport.get.reader(**_kwargs['authorization'])
self._permissions = reader.read()
#
# @TODO: add the login page provided int the configuration with full permissions
# or
# self._permissions = pd.concat([pd.DataFrame([{"user":"*","uri":self._uri(),"allow":1}]),self._permissions])
def inspect (self,path,what) :
if not path or not os.path.exists(path) :
raise Exception (f'Missing {what} {path}')
def authenticate(self,**_args):
print (" ********* ", self._authKey)
_kwargs = copy.copy(_args)
_kwargs['config'] = self._config
# _user = self.login(**_kwargs)
_request = _args['request']
_method = self.module(f'{self._authKey}.login')
_user = _method(_request,self._config)
#
# now we have to find a way to create a cookie that has the content of the
_value = uuid.uuid4().hex
if not _user :
return {},None
#
# The additional parameter we can/should add the following
# max_age, secure, httpsonly, domain
#
if 'age' not in self._config :
self._config['age'] = 3600
return {self._authContext:json.dumps({'token':_value,'username':_user })},{"max_age":self._config['age']}
def allow(self,**_args):
"""
The analysis of permissions is based on propositional logic (permission table) to which the policy is applied i.e how we decide to allow access, we opt for the most restrictive approach
"""
_request = _args['request']
_stream = _request.cookies.get(self._authContext,None)
_user = '*'
_uri = _request.path if 'uri' not in _args else _args['uri']
if _uri.endswith('/') :
_uri = _uri[:-1]
if _stream :
# _object = json.loads(_stream)
# _user = _object['user']
return 1
#
# let's extract the matching criteria from the permissions data
# From the matching criteria we will select the most restrictive
_query=f"""(user=='{_user}' or user=='*') and ((uri=='{_uri}' or uri=='*') or allow==1) """
_values = self._permissions.query(_query).allow.tolist()
if not _values:
#
#
return False
#
# if of the matching criteria we do not have a denial, the system will allow access
#
return 0 not in _values
#
# we should also look into the cookie
# return _uri not in self._config['permissions'] or self._config['permissions'] == '*' or self._config['uri'] == _uri
def module (self,_name):
"""
We need to identify the location of the plugins
"""
_name = _name.strip()
if self._plugins.has(_name):
_m = self._plugins.get(_name)
return copy.deepcopy(_m) if _m else None
return None
def method (self):
return self._config['method']
# def _uri (self) :
# return self._config.get('uri',None)
def loginURI (self):
return '/'.join(self._loginURI) if self._loginURI else None
class parameters:
"""
This is a decorator intended to abstractly handle a plugin
"""
def __init__(self, **_args):
self._input = _args['input']
self._method = ['POST']
# self._model = _args['model']
def __call__(self, _callback):
def _wrapper(*args, **kwargs):
return _callback(*args, **kwargs)
setattr(_wrapper, 'input', self._input)
setattr(_wrapper, 'method', self._method)
return _wrapper

@ -3,13 +3,44 @@ This file will describe how a site is loaded
""" """
from cms.engine.config.structure import Layout, System from cms.engine.config.structure import Layout, System
from cms import disk, cloud from cms import disk, cloud
import cms
import copy import copy
import json import json
import io import io
import os import os
from jinja2 import Environment, BaseLoader, FileSystemLoader from jinja2 import Environment, BaseLoader, FileSystemLoader
from datetime import datetime from datetime import datetime
import mistune
from mistune import markdown
from flask import make_response, render_template
import numpy as np
import pandas as pd
class RequestController :
def __init__(self,_route):
self._routes = _route
def isfile(self,request):
#
# call self.exists to determin if this is an actual file or not
return '.' in request.path
def isroute(self,request):
# return '/'.join(request.path[1:].split('/')[:2]) in self._routes
return self.get_route(request) in self._routes
return False
def isapi(self,request):
return 'api' in request.path
def get_route(self,request):
_items = request.path[1:].split('/')
_items = _items if _items[-1] != '' else _items[:-1]
N = len(_items) + 1
_names = []
for i in range(1, len(_items) + 1):
_names.append('/'.join(_items[:i]))
_names=[_item for _item in _names if _item in self._routes]
return _names[-1] if _names else None
class IOConfig: class IOConfig:
""" """
@ -19,8 +50,11 @@ class IOConfig:
self._config = {'system':{},'layout':{},'plugins':{}} self._config = {'system':{},'layout':{},'plugins':{}}
self._caller = None self._caller = None
self._location= _args['location'] if 'location' in _args else None self._location= _args['location'] if 'location' in _args else None
self._logs = [] self._logs = []
def get(self,_key) : def get(self,_key) :
#
# @TODO: Include elements to be skipped (just in case & for security reasons)
if not _key : if not _key :
return self._config return self._config
_keys = _key.split('.') if '.' in _key else [_key] _keys = _key.split('.') if '.' in _key else [_key]
@ -34,6 +68,11 @@ class IOConfig:
return _object return _object
def set(self,_expr,value): def set(self,_expr,value):
"""
Set the value of an attribute given the path with dot of the attribute
_expr expression of the attribute e.g: person.age
value value to be set for the attribute
"""
if len(_expr.split('.')) == 1 : if len(_expr.split('.')) == 1 :
self._config[_expr] = value self._config[_expr] = value
else: else:
@ -62,7 +101,16 @@ class Initialization (IOConfig):
# #
# Invoke initialization # Invoke initialization
self.reload() self.reload()
#
if self._caller and self._caller.secure :
self.secure = self._caller.secure
else:
self.secure = cms.secure.Manager(config = self._config)
#
# Log initializaton ...
#
self.log(action='init.security',module='site.init',input= self.secure._permissions.to_dict(orient='records'))
def reload (self): def reload (self):
_args = self._args _args = self._args
self._config = self.read_config(**_args) self._config = self.read_config(**_args)
@ -80,6 +128,8 @@ class Initialization (IOConfig):
self.menu() self.menu()
self.plugins() self.plugins()
def read_config(self,**_args) : def read_config(self,**_args) :
_config = {} _config = {}
if 'path' in _args : if 'path' in _args :
@ -104,7 +154,9 @@ class Initialization (IOConfig):
pass pass
_context = self.get('system.context') _context = self.get('system.context')
_context = _context[:-1] if _context.endswith('/') else _context
# _context = f'{_context}/' if _context != "" and not _context.endswith("/") else _context
# _context = f'{_context}/files'
if self._caller : if self._caller :
# #
# There is a parent context we need to account and we are updating the current context to reflect the caller context # There is a parent context we need to account and we are updating the current context to reflect the caller context
@ -114,7 +166,6 @@ class Initialization (IOConfig):
# updating the configuratioin # updating the configuratioin
_iconURI = '/'.join(["",_parentContext,self._caller.get('system.icon')]) _iconURI = '/'.join(["",_parentContext,self._caller.get('system.icon')])
# print ([self._caller.get('system.context'), _parentContext,_context])
if self._caller.get('system.context') != '': if self._caller.get('system.context') != '':
_parentContext = "/"+_parentContext _parentContext = "/"+_parentContext
_context = "/"+_context _context = "/"+_context
@ -162,13 +213,14 @@ class Initialization (IOConfig):
_context = self.get('system.context') _context = self.get('system.context')
_logo = self.get('system.logo') _logo = self.get('system.logo')
_root = self.get('layout.root') _root = self.get('layout.root')
if self.get('system.source.id') == 'cloud' : # if self.get('system.source.id') == 'cloud' :
_icon = f'{_context}/api/cloud/download?doc={_logo}' # _icon = f'{_context}/api/cloud/download?doc={_logo}'
else: # else:
_icon = f'{_context}/api/disk/read?uri={_logo}' # _icon = f'{_context}/api/disk/read?uri={_logo}'
# _root = f'{_context}/api/disk/read?uri={_root}' _icon = f'{_context}/{_logo}'.replace(_root,'')
# self.set('layout.root',_root)
self.set('system.icon',_icon) self.set('system.icon',_icon)
self.set('system.logo',_icon) self.set('system.logo',_icon)
# #
@ -215,10 +267,6 @@ class Initialization (IOConfig):
# #
# At this point the entire menu is build and we need to have it sorted # At this point the entire menu is build and we need to have it sorted
self.order() self.order()
# _labels = list(self.get('layout.menu').keys())
# self.log(action='init.menu',module='menu',input=_labels)
# print (self.get('layout.menu'))
# print (_object)
def order(self,**_args): def order(self,**_args):
if self.get('layout.order.menu') : if self.get('layout.order.menu') :
_sorted = {} _sorted = {}
@ -269,12 +317,14 @@ class Initialization (IOConfig):
_parentContext = self.get('system.parentContext') _parentContext = self.get('system.parentContext')
_map = {} _map = {}
_plugins = {} _plugins = {}
if self.get('system.source.id') == 'cloud' : # if self.get('system.source.id') == 'cloud' :
_plugins = cloud.plugins(_context) # _plugins = cloud.plugins(_context)
else: # else:
_plugins = disk.plugins(context=_context) # _plugins = disk.plugins(context=_context)
_uri = 'api/system/debug' _uri = f'{_context}/api/system/debug'
_uri = _uri if not _context else f'{_context}/{_uri}' if _uri.startswith('/') :
_uri = _uri[1:]
# _uri = _uri if not _context else f'{_context}/{_uri}'
_plugins[_uri] = self.debug _plugins[_uri] = self.debug
if os.path.exists(_folder) and self.get('plugins'): if os.path.exists(_folder) and self.get('plugins'):
@ -289,6 +339,9 @@ class Initialization (IOConfig):
if _pointer : if _pointer :
_uri = f"api/{_filename}/{_module}" _uri = f"api/{_filename}/{_module}"
_uri = f"{_context}/{_uri}" if _context else _uri _uri = f"{_context}/{_uri}" if _context else _uri
if (_uri.startswith("/")) :
_uri = _uri[1:]
_map[_uri] = _pointer _map[_uri] = _pointer
if _parentContext : if _parentContext :
# _uri = f"{_parentContext}/{_context}" # _uri = f"{_parentContext}/{_context}"
@ -306,7 +359,9 @@ class Initialization (IOConfig):
# Updating plugins from disk/cloud # Updating plugins from disk/cloud
_plugins = _map if not _plugins else dict(_plugins,**_map) _plugins = _map if not _plugins else dict(_plugins,**_map)
# #
# if we have login enabled we should add them as a plugins
# #
self.set('plugins',_plugins) self.set('plugins',_plugins)
self.log(action='init.plugins',module='plugins',input=list(_plugins.keys())) self.log(action='init.plugins',module='plugins',input=list(_plugins.keys()))
@ -314,39 +369,274 @@ class Site(Initialization) :
def __init__(self,**_args): def __init__(self,**_args):
super().__init__(**_args) super().__init__(**_args)
self._config['system']['portal'] = (self.get('system.routes')) == None self._config['system']['portal'] = (self.get('system.routes')) == None
def html(self,_uri,_id) : self._routes = []
_handler = cloud if self.get('system.source.id') == 'cloud' else disk if self.get('system.routes') :
_html = _handler.html(_uri, self.get(None)) self._routes = list(self.get('system.routes').keys())
return " ".join([f'<div id="{_id}"> ',_html,"</div>"]) if self._caller :
def read(self,_uri) : self._routes = self._caller._routes
self.inspect = RequestController(self._routes)
#
# let's update the plugins
#
if self.secure.method() in ['oauth2.0','oauth20','oauth2'] :
_plugins = self.get('plugins')
_plugins['api/oauth2/authorize'] = cms.authorizationURL
_plugins['api/oauth2/final'] = cms.oauthFinalize
self.set('plugins',_plugins)
def exists (self,uri):
path = []
if self.get('layout.location'):
path.append(self.get('layout.location'))
if self.get('layout.root') not in uri :
path.append(self.get('layout.root'))
path.append(uri)
return os.path.exists( os.sep.join(path))
# def _html(self,_uri,_id,request) :
# if self.secure.allow(request=request):
# _handler = cloud if self.get('system.source.id') == 'cloud' else disk
# _html = _handler.html(_uri, self.get(None))
# #
# # maybe apply the environment here ??
# return " ".join([f'<div id="{_id}"> ',_html,"</div>"])
# else:
# None
def mimeType(self,uri):
_extension = uri.split('.')[-1].strip().lower()
_mimeType = 'application/octet-stream'
if _extension in ['css','js','csv','html','md'] :
_mimeType = f'text/{_extension}'.replace('md','html')
if _extension == 'js' :
_mimeType = 'text/javascript'
elif _extension in ['png','jpg','jpeg'] :
_mimeType = f'image/{_extension}'
return _mimeType
def path(self,_uri):
path = []
if self.get('layout.location'):
path.append(self.get('layout.location'))
if self.get('layout.root') not in _uri :
path.append(self.get('layout.root'))
path.append(_uri)
return os.sep.join(path)
def html (self,_request):
_uri = self.uri(_request)
_mimeType = self.mimeType(_uri)
f = open(self.path(_uri),'r')
_content = f.read() #_handler.html(_uri, self.get(None))
f.close()
if 'md' in _mimeType or 'html' in _mimeType :
# _content = f'<div>{_content}</div>'
if _uri.split('.')[-1].strip().lower() == 'md':
_content = mistune.html(_content).replace("&quot;",'"').replace("&lt;","<").replace("&gt;",">") if _uri[-2:] in ['md','MD','Md','mD'] else _content
_content = f"<div>{_content}</div>" #if 'dom' not in _request.headers else f'<div id="{_request.headers['dom']}">{_content}</div>'
else:
_env = Environment(loader=BaseLoader()).from_string(_content)
_content = str(_env.render(**self.get(None)))
pass
return _content,_mimeType
def uri(self,request):
if 'uri' in request.headers or 'uri' in request.args :
return request.headers['uri'] if 'uri' in request.headers else request.args.get('uri',None)
else:
_route = self.inspect.get_route(request)
#
# The route doesn't have any forward slashes nor slash at the end
#
file = request.path.replace(f'{_route}','')
#
if file.startswith('//') or file.startswith('/') :
# NOTE: false positive if file.startswith('/') is used
file = file[2:] if file.startswith('//') else file[1:]
return file if file.strip() not in ['',None,'/'] else self.get('layout.index')
# # file = '/'.join(request.path[1:].split('/')[1:])
# file = request.path[1:]
# _isapi = 'api' in request.path
# _isroute = '/'.join(request.path[1:].split('/')[:2]) in self._routes
# _isfile = '.' in request.path
# return file if _isfile and not _isroute else self.get('layout.index')
def read(self,request) :
_kwargs = {'allow':0}
# if self.secure.allow(request=request):
_uri = self.uri(request)
_handler = cloud if self.get('system.source.id') == 'cloud' else disk _handler = cloud if self.get('system.source.id') == 'cloud' else disk
return _handler.read(uri=_uri, config=self.get(None)) _extension = _uri.split('.')[1].lower()
_mimeType = self.mimeType(_uri)
if _extension.strip() in ['md','txt','html','js','css'] :
_content,_mimeType = self.html(request)
else:
#
# Opening a binary file
f = open(self.path(_uri),'rb')
_content = io.BytesIO(f.read())
f.close()
# _content,_ = _handler.read(uri=_uri, config=self.get(None))
_kwargs = {'allow':1,'mimeType':_mimeType,'extension':_extension,
'path':self.path(_uri),
'uri':_uri,'request':request.path}
self.log(action='file.read',module='site.read',input=_kwargs)
if _content :
return _content, _mimeType
return None, 'plain/html'
# if 'html' in _mimetype :
# _args = {'layout':self.get('layout'),'system':self.get('system')}
# _content = _content.decode('utf-8') if type(_content) == bytes else _content
# _env = Environment(loader=BaseLoader()).from_string(_content)
# _content = _env.render(**_args)
# return _content,_mimetype
# return None,'plain/html'
def render (self,**_args):
"""
:id target of the dom or jinja
:request incoming request
:uri uri to read from disk ...
"""
# _site = self._sites[_appid] if _appid else self._sites[self._id]
_id = _args.get('id')
# _uri= _args['uri']
_request = _args['request']
_uri = self.uri(_request)
_kwargs = {'layout':self.get('layout')}
_system = self.get('system')
for k in ['source','app'] :
if k in _system :
del _system[k]
_kwargs['system'] = _system
_cookies = json.loads(_request.cookies.get(self.secure._authContext ,"{}"))
if _cookies :
_kwargs['username'] = _cookies['username']
_html,_mimeType = self.html(_request)
# _kwargs[_id] = f'<div id="{_id}">{_html}</div>'
# return _kwargs
# _html = f'<div id="{_id}">{_html}</div>'
if _html :
_env = Environment(loader=BaseLoader()).from_string(f'<div id="{_id}">{_html}</div>')
_kwargs[_id] = str(_env.render(**_kwargs))
return _kwargs
return None
def apply_tags (self,_html):
_kwargs = {'layout':self.get('layout'),'system':self.get('system')}
_env = Environment(loader=BaseLoader()).from_string(f'<div>{_html}</div>')
return _env.render(**_kwargs)
def run(self,_request) :
_plugins = self.get('plugins')
_data = "<div align='center'><h2>404</h2></div>"
_mimeType = 'plain/html'
_code = 404
_key = _request.path[1:] #if self.get('system.context') != '' else _request.path[1:]
# print ([_key,_key in list(_plugins.keys())])
# print (list (_plugins.keys()))
if _plugins and _key in _plugins:
_mimeType = 'application/octet-stream'
_pointer = _plugins.get(_key)
if hasattr(_pointer,'mimetype'):
_mimeType = _pointer.mimetype
_data = _pointer(request=_request,config=self.get(None))
if hasattr(_pointer,'method') and _request.method not in _pointer.method :
_data = "<div align='center'><h2>404</h2></div>"
elif not hasattr(_pointer,'mimetype'):
_data,_mimeType = _pointer(request=_request,config=self.get(None))
if 'html' in _mimeType :
_data = self.apply_tags(_data)
if type(_data) == pd.DataFrame :
_data = _data.to_json(orient='records')
elif type(_data) in [dict,list] :
_data = json.dumps(_data)
# #
# # return the ata
_code = _code if not _data else 200
# resp = make_response(_data)
# resp.status_code = _code
# resp.headers['Content-Type'] = _mimeType
# resp.headers['Content-Disposition'] = 'inline'
return _data,_code,{"Content-Type":_mimeType}
class QCMS: class QCMS:
def __init__(self,**_args): def __init__(self,**_args):
_app = Site(**_args) _app = Site(**_args)
self._id = _app.get('system.context') #if _app.get('system.context') else 'main' self._id = _app.get('system.context') #if _app.get('system.context') else 'main'
self._sites = {self._id:_app} # if self._id == '' :
# self._id = '/'
self._sites = {self._id:_app, '/':_app}
self._routes = []
if _app.get('system.routes') : if _app.get('system.routes') :
_routes = _app.get('system.routes') _routes = _app.get('system.routes')
for _name in _routes : for _name in _routes :
self._routes.append(_name)
_path = _routes[_name]['path'] _path = _routes[_name]['path']
self._sites[_name] = Site(context=_name,path=_path,caller=_app) self._sites[_name] = Site(context=_name,path=_path,caller=_app)
def render(self,_uri,_id,_appid=None): # self._sites[f'{_name}/'] = self._sites[_name]
_site = self._sites[_appid] if _appid else self._sites[self._id]
_args = {'layout':_site.get('layout')}
_system = _site.get('system') self.inspect = RequestController(self._routes)
for k in ['source','app'] :
if k in _system : def _render(self,request):
del _system[k] _site = self.get(request)
_args['system'] = _system _args = _site.render(request=request,id='index')
_html = _site.html(_uri,_id) return render_template('index.html',**_args)
def _read(self,request):
_env = Environment(loader=BaseLoader()).from_string(_html) _site = self.get(request)
_args[_id] = str(_env.render(**_args)) return _site.read(request)
return _args def delegate(self,request):
isfile = self.inspect.isfile(request)
isapi = self.inspect.isapi(request)
isroute= self.inspect.isroute(request)
if not isapi and not isfile:
return self._render(request)
#
# # let's check on files to be services (assumption is that they should have an extension)
# The following propositional logic is as such
# (isfile and isroute) or (isfile and not isroute) -> isfile (isroute is optional)
if isfile :
_content,_mimeType = self._read(request)
return _content,200,{'Content-Type':_mimeType}
if isapi :
_site = self.get(request)
return _site.run(request)
def get(self,request=None) :
if request :
if self.inspect.isroute(request) :
self._id = self.inspect.get_route(request)
else:
self._id = ''
else:
self._id = ''
return self._sites[self._id]
def set(self,_id): def set(self,_id):
self._id = _id self._id = _id
def get(self,_id=None): def allow(self,id, request) :
return self._sites[self._id] if not _id else self._sites[_id] _site = self.get(id)
return _site.secure.allow(request=request)
def has(self,_id):
return _id in self._sites

@ -0,0 +1,28 @@
.qcms-login {display:grid; grid-template-rows: 64px auto 64px; gap:4px; padding:8px}
.qcms-login .border {border:4px solid #CAD5E0}
.qcms-login .header {display:grid; grid-template-columns: 80px auto; align-items: center;}
.qcms-login .header img{ height:64px; margin:4px;}
.qcms-login-buttons {display:grid;
grid-template-columns: 50% 50%; gap:4px;
width:100%;
align-items:center;
}
.qcms-login-buttons .fa-times {color:maroon}
.qcms-login-input {
display:grid;
grid-template-rows: 64px 64px; gap:4px;
}
.qcms-login-input INPUT{
padding:8px;
border:4px solid transparent;
background-color: #f3f3f3;
outline: 0;
}
.qcms-login-input INPUT:focus {
border-left-color: #4682B4;;
}
.qcms-login .small {font-weight: lighter; font-size:11px}

@ -14,10 +14,14 @@ qcms.menu.Common = function (){
var http = HttpClient.instance() var http = HttpClient.instance()
http.setHeader('dom',_domId) http.setHeader('dom',_domId)
http.setHeader('uri',uri) http.setHeader('uri',uri)
http.post(`${qcms.context}/page`,(x)=>{ http.post(`${qcms.context}/${uri}`,(x)=>{
// //
// @TODO: In case of an error // @TODO: In case of an error
var _dom = $(x.responseText) var _dom = $(x.responseText)
if ( $(_dom).attr("id") == null){
$(_dom).attr("id",_domId)
}
if($(`${_parentId} #${_domId}`).length){ if($(`${_parentId} #${_domId}`).length){
$(`${_parentId} #${_domId}`).remove() $(`${_parentId} #${_domId}`).remove()
} }
@ -59,6 +63,7 @@ qcms.menu.Basic = function (_layout,_outputId,_domId){
// var _finalize = this._finalize // var _finalize = this._finalize
// var _domId = this._domId // var _domId = this._domId
var _me = this ; var _me = this ;
_items.forEach(_item=>{ _items.forEach(_item=>{
var _div = jx.dom.get .instance('DIV') var _div = jx.dom.get .instance('DIV')
@ -113,8 +118,9 @@ qcms.menu.Basic = function (_layout,_outputId,_domId){
*/ */
var _names = _layout.order.menu.length > 0 ? _layout.order.menu : Object.keys(_layout.menu) var _names = _layout.order.menu.length > 0 ? _layout.order.menu : Object.keys(_layout.menu)
_names.forEach ((_name)=>{ _names.forEach ((_name)=>{
var _div = this._build(_layout.menu[_name]) ; if (_layout.menu[_name]){
var _div = this._build(_layout.menu[_name]) ;
var _sub = jx.dom.get.instance('DIV') var _sub = jx.dom.get.instance('DIV')
var _menuItem = jx.dom.get.instance('DIV') var _menuItem = jx.dom.get.instance('DIV')
@ -126,7 +132,7 @@ qcms.menu.Basic = function (_layout,_outputId,_domId){
_menuItem.appendChild(_sub) _menuItem.appendChild(_sub)
_domId = (_domId == null || $(_domId).length == 0)?'.main .menu' : _domId _domId = (_domId == null || $(_domId).length == 0)?'.main .menu' : _domId
$(`${_domId}`).append(_menuItem) $(`${_domId}`).append(_menuItem)
}
}) })
} }

@ -5,7 +5,12 @@ qcms.page = {}
qcms.page.Observer = function (_parentId,_domId,_uri){ qcms.page.Observer = function (_parentId,_domId,_uri){
this._parentId = _parentId == null?'':_parentId this._parentId = _parentId == null?'':_parentId
this._domId = _domId this._domId = _domId
this._uri = _uri this._uri = _uri
if (qcms.root){
//-- bug fix for some adjustments that were needed for streamlined version
this._uri = this._uri.replace(`${qcms.root}/`,'')
}
this.finalize = function(_id){ this.finalize = function(_id){
var _script = $(`${_id} script`) var _script = $(`${_id} script`)
@ -26,11 +31,12 @@ qcms.page.Observer = function (_parentId,_domId,_uri){
var http = HttpClient.instance() var http = HttpClient.instance()
http.setHeader('dom',this._domId) http.setHeader('dom',this._domId)
http.setHeader('uri',this._uri) // http.setHeader('uri',this._uri)
var _uri = this._uri var _uri = `${qcms.context}/${this._uri}`
http.post(`${qcms.context}/page`,function(x){
http.post(_uri,function(x){
var _dom = $(x.responseText) var _dom = $(x.responseText)
var _found = qcms.html.hasNode( $(_id), $(_dom)) var _found = qcms.html.hasNode( $(_id), $(_dom))
if (_found == 0){ if (_found == 0){

@ -23,18 +23,8 @@ Vanderbilt University Medical Center
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="x12,parser,healthcare,tools,informatics,research,phd,post-doc,api,python, jamia,amia,pnas,nature"> <meta name="keywords" content="x12,parser,healthcare,tools,informatics,research,phd,post-doc,api,python, jamia,amia,pnas,nature">
<meta name="robots" content="/, follow, max-snippet:-1, max-image-preview:large"> <meta name="robots" content="/, follow, max-snippet:-1, max-image-preview:large">
<link href="{{system.context}}/static/css/themes/{{system.theme}}" rel="stylesheet" type="text/css"> {%include "libs.html" %}
<link href="{{system.context}}/static/css/default.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/static/css/icons.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/static/css/border.css" rel="stylesheet" type="text/css">
<script src="{{system.context}}/static/js/jx/dom.js"></script>
<script src="{{system.context}}/static/js/jx/utils.js"></script>
<script src="{{system.context}}/static/js/jx/rpc.js"></script>
<script src="{{system.context}}/static/js/jx/ext/modal.js"></script>
<script src="{{system.context}}/static/js/jquery/jquery.js"></script>
<script src="{{system.context}}/static/js/menu.js"></script>
<script src="{{system.context}}/static/js/search.js"></script>
<script src="{{system.context}}/static/js/fontawesome/js/all.js"></script>
</head> </head>
<style> <style>
.dialog { .dialog {
@ -42,30 +32,38 @@ Vanderbilt University Medical Center
padding:4px; padding:4px;
} }
.fa-circle-xmark {color:maroon} .fa-circle-xmark {color:maroon}
</style> </style>
<script> <script>
$(document).ready(function(){ $(document).ready(function(){
_html = jx.dom.get.value('dialog') _html = jx.dom.get.value('dialog')
jx.dom.hide('dialog') jx.dom.hide('dialog')
jx.modal.show(_html) jx.modal.show({id:'foo', html:_html})
}) })
</script> </script>
<body> <body>
<div class="main">
<div class="header">
{%include "header.html" %}
</div>
<div id="dialog" class="dialog">
<div align="center" class="large-text" style="font-size:48px; font-weight: bold;">
<p><i class="fa-regular fa-circle-xmark"></i> 404
</p>
<div id="dialog" style="display:none; ">
<div class="border-round border" style="background-color: #f3f3f3; display:grid; grid-template-columns: 32px auto; align-items:center; gap:8px;">
<img src="{{system.icon}}" style="height:28px; width:28px; margin:4px"/>
Error - {{layout.header.title}}
</div>
<p>
<div style="display:grid; grid-template-columns:48px auto; gap:4px; width:350px;">
<div align="center">
<i class="fa-solid fa-bug" style="color:brown; font-size:48px"></i>
</div> </div>
<p></p> <div align="center">
<div class="border-top" style="padding:8px"> <div class="bold large-text" style="font-size:32px;">404</div>
<div>Page Not found</div>
<div>Page Not Found or Content Unavailable</div>
</div> </div>
</div> </div>
</p>
</div> </div>
</body></html> </body>
</html>

@ -0,0 +1,69 @@
<!--
(c) 2004 - 2022 Health Information Privacy Laboratory
Vanderbilt University Medical Center
This is a flask-based cms that considers the following for a website :
- header
- content
- menu
- content
- footer
-->
<!DOCTYPE html>
<html lang="en">
<head >
<title>{{layout.header.title}}</title>
<link rel="shortcut icon" href="{{system.context}}/favicon.ico">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="x12,parser,healthcare,tools,informatics,research,phd,post-doc,api,python, jamia,amia,pnas,nature">
<meta name="robots" content="/, follow, max-snippet:-1, max-image-preview:large">
{%include "libs.html" %}
</head>
<style>
.dialog {
width:700px;
padding:4px;
}
.fa-circle-xmark {color:maroon}
</style>
<script>
$(document).ready(function(){
_html = jx.dom.get.value('dialog')
jx.dom.hide('dialog')
jx.modal.show({id:'foo', html:_html})
})
</script>
<body>
<div id="dialog" style="display:none; ">
<div class="border-round border" style="background-color: #f3f3f3; display:grid; grid-template-columns: 32px auto; align-items:center; gap:8px;">
<img src="{{system.icon}}" style="height:28px; width:28px; margin:4px"/>
Error - {{layout.header.title}}
</div>
<p>
<div style="display:grid; grid-template-columns:48px auto; gap:4px; width:350px;">
<div align="center">
<i class="fa-solid fa-bug" style="color:brown; font-size:48px"></i>
</div>
<div align="center">
<div class="bold large-text" style="font-size:32px;">500</div>
<div>Page Not found</div>
</div>
</div>
</p>
</div>
</body>
</html>

@ -15,7 +15,7 @@ Vanderbilt University Medical Center
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head > <head >
{% include "libs.html"%}
<title>{{layout.header.title}}</title> <title>{{layout.header.title}}</title>
<link rel="shortcut icon" href="{{system.icon}}"> <link rel="shortcut icon" href="{{system.icon}}">
@ -24,55 +24,7 @@ Vanderbilt University Medical Center
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="keywords" content="quick cms, cms, python, flask, qcms"> <meta name="keywords" content="quick cms, cms, python, flask, qcms">
<meta name="robots" content="/, follow, max-snippet:-1, max-image-preview:large"> <meta name="robots" content="/, follow, max-snippet:-1, max-image-preview:large">
<!-- <link href="{{system.context}}/static/css/default.css" rel="stylesheet" type="text/css"> -->
<!-- <link href="{{system.context}}/static/css/menu.css" rel="stylesheet" type="text/css"> -->
<!-- <link href="{{system.context}}/static/css/border.css" rel="stylesheet" type="text/css"> -->
<!-- <link href="{{system.context}}/static/css/animation/_ocean.css" rel="stylesheet" type="text/css"> -->
<!-- <link href="{{system.context}}/static/css/themes/{{system.theme}}" rel="stylesheet" type="text/css"> -->
<!-- <link href="{{system.context}}/static/css/icons.css" rel="stylesheet" type="text/css"> -->
<link href="{{system.parentContext}}/static/css/icons.css" rel="stylesheet" type="text/css">
<link href="{{system.parentContext}}/static/css/source-code.css" rel="stylesheet" type="text/css">
<link href="{{system.parentContext}}/static/css/search.css" rel="stylesheet" type="text/css">
<link href="{{system.parentContext}}/static/css/dialog.css" rel="stylesheet" type="text/css">
<link href="{{system.parentContext}}/static/css/dashboard.css" rel="stylesheet" type="text/css">
<!-- applying themes as can -->
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/layout.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/header.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/menu.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/borders.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/footer.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/pane.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/api/disk/read?uri={{layout.root}}/_assets/themes/{{system.theme}}/responsive.css" rel="stylesheet" type="text/css">
<!-- -->
<meta property="og:title" content="{{layout.header.title}}" />
<meta property="og:type" content="website" />
<!-- <meta property="og:url" content="https://www.yourwebsite.com/page" /> -->
<meta property="og:image" content="{{system.logo}}" />
<meta property="og:image:alt" content="{{layout.header.title}}" />
<meta property="og:description" content="{{layout.header.title}}; {{layout.header.subtitle}}, version {{system.version}}" />
<meta property="og:site_name" content="{{layout.header.title}}" />
<script src="{{system.parentContext}}/static/js/qcms/qcms.js"></script>
<script src="{{system.parentContext}}/static/js/qcms/menu.js"></script>
<script src="{{system.parentContext}}/static/js/qcms/page-loader.js"></script>
<script src="{{system.parentContext}}/static/js/qcms/dialog.js"></script>
<script src="{{system.parentContext}}/static/js/dashboard.js"></script>
<script src="{{system.parentContext}}/static/js/jx/dom.js"></script>
<script src="{{system.parentContext}}/static/js/jx/utils.js"></script>
<script src="{{system.parentContext}}/static/js/jx/rpc.js"></script>
<script src="{{system.parentContext}}/static/js/jx/ext/modal.js"></script>
<script src="{{system.parentContext}}/static/js/jx/ext/math.js"></script>
<script src="{{system.parentContext}}/static/js/jquery/jquery.js"></script>
<!-- <script src="{{system.parentContext}}/static/js/menu.js"></script> -->
<script src="{{system.parentContext}}/static/js/search.js"></script>
<!-- <script src="{{system.parentContext}}/static/js/bootup.js"></script> -->
<!-- <script src="{{system.parentContext}}/static/js/dialog.js"></script> -->
<script src="{{system.parentContext}}/static/js/apexcharts/apexcharts.min.js"></script>
<script src="{{system.parentContext}}/static/js/fontawesome/js/all.js"></script>
</head> </head>
<script> <script>
// sessionStorage.setItem('{{system.id}}','{{system.context|safe}}') // sessionStorage.setItem('{{system.id}}','{{system.context|safe}}')
@ -82,8 +34,7 @@ Vanderbilt University Medical Center
// var qcms = {} // var qcms = {}
// } // }
qcms.context = '{{system.context}}' qcms.context = '{{system.context}}'
// dialog.context = '{{system.context}}' qcms.root = '{{layout.root}}'
//sessionStorage.setItem('{{system.id}}','{{system.context|safe}}')
$(document).ready( function(){ $(document).ready( function(){
bootup.init('{{system.id}}',_layout) bootup.init('{{system.id}}',_layout)
@ -100,9 +51,9 @@ Vanderbilt University Medical Center
{%include "header.html" %} {%include "header.html" %}
</div> </div>
<div id="menu" class="menu">
{%include "menu.html" %} <div id="menu" class="menu">{%include "menu.html" %}</div>
</div>
<div id="content" class="content"> <div id="content" class="content">
{%include "content.html" %} {%include "content.html" %}

@ -0,0 +1,43 @@
<link href="{{system.parentContext}}/static/css/icons.css" rel="stylesheet" type="text/css">
<link href="{{system.parentContext}}/static/css/source-code.css" rel="stylesheet" type="text/css">
<link href="{{system.parentContext}}/static/css/search.css" rel="stylesheet" type="text/css">
<link href="{{system.parentContext}}/static/css/dialog.css" rel="stylesheet" type="text/css">
<link href="{{system.parentContext}}/static/css/dashboard.css" rel="stylesheet" type="text/css">
<link href="{{system.parentContext}}/static/css/qcms-login.css" rel="stylesheet" type="text/css">
<!-- applying themes as can -->
<link href="{{system.context}}/_assets/themes/{{system.theme}}/layout.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/_assets/themes/{{system.theme}}/header.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/_assets/themes/{{system.theme}}/menu.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/_assets/themes/{{system.theme}}/borders.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/_assets/themes/{{system.theme}}/footer.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/_assets/themes/{{system.theme}}/pane.css" rel="stylesheet" type="text/css">
<link href="{{system.context}}/_assets/themes/{{system.theme}}/responsive.css" rel="stylesheet" type="text/css">
<!-- -->
<meta property="og:title" content="{{layout.header.title}}" />
<meta property="og:type" content="website" />
<!-- <meta property="og:url" content="https://www.yourwebsite.com/page" /> -->
<meta property="og:image" content="{{system.logo}}" />
<meta property="og:image:alt" content="{{layout.header.title}}" />
<meta property="og:description" content="{{layout.header.title}}; {{layout.header.subtitle}}, version {{system.version}}" />
<meta property="og:site_name" content="{{layout.header.title}}" />
<script src="{{system.parentContext}}/static/js/qcms/qcms.js"></script>
<script src="{{system.parentContext}}/static/js/qcms/menu.js"></script>
<script src="{{system.parentContext}}/static/js/qcms/page-loader.js"></script>
<script src="{{system.parentContext}}/static/js/qcms/dialog.js"></script>
<script src="{{system.parentContext}}/static/js/dashboard.js"></script>
<script src="{{system.parentContext}}/static/js/jx/dom.js"></script>
<script src="{{system.parentContext}}/static/js/jx/utils.js"></script>
<script src="{{system.parentContext}}/static/js/jx/rpc.js"></script>
<script src="{{system.parentContext}}/static/js/jx/ext/modal.js"></script>
<script src="{{system.parentContext}}/static/js/jx/ext/math.js"></script>
<script src="{{system.parentContext}}/static/js/jquery/jquery.js"></script>
<script src="{{system.parentContext}}/static/js/qcms/qcms-login.js"></script>
<!-- <script src="{{system.parentContext}}/static/js/menu.js"></script> -->
<script src="{{system.parentContext}}/static/js/search.js"></script>
<!-- <script src="{{system.parentContext}}/static/js/bootup.js"></script> -->
<!-- <script src="{{system.parentContext}}/static/js/dialog.js"></script> -->
<script src="{{system.parentContext}}/static/js/apexcharts/apexcharts.min.js"></script>
<script src="{{system.parentContext}}/static/js/fontawesome/js/all.js"></script>

@ -0,0 +1,32 @@
<title>{{layout.header.title}} - Authentication</title>
<link rel="shortcut icon" href="{{system.icon}}">
{%include "libs.html" %}
<script>
$(document).ready(()=>{
qcms.login.model = "{{login.model|safe}}"
qcms.login.cancel()
})
</script>
<style>
.qcms-login-error {
display:grid;
grid-template-columns: 32px auto;
align-items: center;
gap:8px;
padding:4px; color:maroon;
font-weight:lighter; font-size:12px;
}
</style>
<div class="border-round border" style="margin-top:15%; margin-left:35%; margin-right:35%">
{{html|safe}}
<div class="qcms-login-error-frame">
<div class="qcms-login-error border-round border">
<div class="border-right active" align="center" onclick="qcms.login.cancel()">
<i class="fa-solid fa-times" style="color:maroon"></i>
</div>
<div>Login error, please try again</div>
</div>
</div>
</div>

@ -0,0 +1,24 @@
{%include "libs.html" %}
<div class="qcms-login">
<div class="header">
<div><img src="{{system.icon}}"/></div>
<div class="bold">
{{layout.header.title|safe}}
<div class="small">qcms - nextcloud</div>
</div>
</div>
<div class="qcms-login-input">
<input type="text" class="username" placeholder="username or email"/>
<input type="password" class="token" placeholder="password or token"/>
</div>
<div class="qcms-login-buttons">
<div class="border-round border bold" onclick="qcms.login.cancel()"><div class="active"><i class="fa-solid fa-times" style="color:maroon"></i> Cancel</div></div>
<div class="border-round border bold" onclick="qcms.login.authenticate()"><div class="active" ><i class="fa-solid fa-check" style="color:green"></i> Login</div></div>
</div>
</div>

@ -0,0 +1,71 @@
{%include "libs.html" %}
<script>
var oauth2 = {popup:null,}
oauth2.listen = function (){
oauth2._handler = setInterval (()=>{
try{
// console.log(oauth2.popup.location.href.match(/code=/) != null )
if (oauth2.popup.closed){
if (oauth2.popup.sessionStorage.redirect) {
if (oauth2._handler){
clearInterval(oauth2._handler)
}
window.open(oauth2.popup.sessionStorage.redirect,'_self')
}
}
}catch(e){
console.log(e)
if (oauth2._handler){
clearInterval(oauth2._handler)
}
}
},1500);
}
oauth2.init = function (){
var uri = ([qcms.context,'api/oauth2/authorize']).join('/')
var http = HttpClient.instance()
http.post(uri,(x)=>{
if(x.status == 200 && x.readyState == 4){
_data = JSON.parse(x.responseText)
$('.qcms-login-buttons .provider').html(_data.label)
$('.qcms-login').attr('data',_data.url)
}
})
}
oauth2.login = function (){
var url = $('.qcms-login').attr('data')
oauth2.popup = window.open(url, 'oauth', 'width=405, height=900')
oauth2.popup.focus()
oauth2.listen()
//
// @TODO: add a listener
}
$(document).ready(()=>{
oauth2.init()
})
</script>
<div class="qcms-login">
<div class="header">
<img src="{{system.icon}}"/>
<div>
{{layout.header.title}}
</div>
</div>
<div class="qcms-login-input">
<ul style=";">
<div style="margin:2px"><i class="fa-solid fa-minus"></i> Login using an account</div>
<div style="margin:2px"><i class="fa-solid fa-minus"></i> An authetnication window will pop-out</div>
</ul>
</div>
<div class="qcms-login-buttons" style="display:block">
<div class="border-round border" onclick="oauth2.login()"><div class="active"><i class="fa-solid fa-check" style="color:green"></i> <span class="provider bold"></span> </div></div>
</div>
</div>

@ -0,0 +1,19 @@
{%include "libs.html" %}
<div class="qcms-login">
<div class="header">
<img src="{{system.icon}}"/>
<div>
{{layout.header.title}}
</div>
</div>
<div class="qcms-login-input">
<input type="text" class="username" placeholder="username"/>
<input type="password" class="password" placeholder="password"/>
</div>
<div class="qcms-login-buttons">
<div class="border-round border" onclick="qcms.login.cancel()"><div class="active"><i class="fa-solid fa-times" style="color:maroon"></i> Cancel</div></div>
<div class="border-round border" onclick="qcms.login.authenticate()"><div class="active"><i class="fa-solid fa-check" style="color:green"></i> Login</div></div>
</div>
</div>

@ -0,0 +1,7 @@
<div class="border-round border" style=" background-color:#FFFFFF; display:grid; grid-template-columns: auto 64px; gap:8px; padding:8px">
<div>Hi, {{username}}</div>
<div align="center">
<div class="active"><i class="fa-solid fa-power-off" style="color:maroon"></i></div>
</div>
</div>

@ -18,7 +18,7 @@ classifiers = [
] ]
dependencies = [ dependencies = [
"flask", "flask",
"gitpython", "gitpython", "flask-jwt-extended",
"termcolor", "termcolor",
"flask-session", "flask-session",
"mistune","plugin-ix@git+https://github.com/lnyemba/plugins-ix", "mistune","plugin-ix@git+https://github.com/lnyemba/plugins-ix",
@ -41,10 +41,10 @@ Homepage = "https://healthcareio.the-phi.com/git/code/transport.git"
[tool.setuptools] [tool.setuptools]
include-package-data = true include-package-data = true
zip-safe = false zip-safe = false
script-files = ["bin/qcms"] script-files = ["bin/qcms","bin/qcms.bat"]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
include = ["meta","meta.*", "cms", "cms.*"] include = ["cms", "cms.*"]
[tool.setuptools.dynamic] [tool.setuptools.dynamic]
version = {attr = "cms.meta.__version__"} version = {attr = "cms.meta.__version__"}

Loading…
Cancel
Save