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)
# def add_login(manifest:Annotated[str,typer.Argument(help="path of the manifest file")],) :
# pass
def load(**_args):
"""
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 apexchart
from . import meta
from . import secure
class Plugin :
#
# decorator for plugin functions, this is a preliminary to enable future developement
@ -29,46 +29,7 @@ class Plugin :
setattr(wrapper,'method',self._method)
setattr(wrapper,'mimetype',self._mimetype)
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
def call(**_args):
"""
@ -124,66 +85,63 @@ class Plugin :
return _data,_code,{'Content-Type':_mimeType}
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)
#
# _context = _handler.system()['context']
_context = _handler.get('system.context')
if _context :
_uri = f'{_context}/{_uri}'
# print ([' ** ',_uri, _args['uri']])
# _plugins = _handler.plugins()
_plugins = _handler.get('plugins')
_code = 200
if _uri in _plugins :
# _config = _handler.config() #_args['config']
_config = _handler.get(None) #_args['config']
#
# default plugins to load into the configuration file
@Plugin(mimetype="application/json")
def authorizationURL (**_args):
# _config = _args['config']
_source = _args['config']['system']['source']
#
#
if 'secure' in _source :
_path = _source['secure']['path']
f = open(_path)
_config = json.loads(f.read())
if _config['method'] in ['oauth2','oauth20','oauth2.0'] :
_url = [f"{_key}={_config[_key]}" for _key in _config if _key not in ['method','authorization_url']]
# return _url
_url = _config['authorization_url']+'?'+'&'.join(_url)
return {"url":_url,"label":f"Login with {_source['secure']['provider']}"}
else:
return {}
_pointer = _plugins[_uri]
_data = {}
if hasattr(_pointer,'mimetype') and hasattr(_pointer,'method'):
#
# 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)
else:
_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>
//
// I should
_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>'
</script>
"""
# if _mimeType in ['text/html','plain/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}
@Plugin(mimetype="text/html")
def oauthFinalize (**_args):
_request = _args['request']
_html = """
<script src="/static/js/fontawesome/js/all.js"></script>
<script src="/static/js/jx/rpc.js"></script>
<script>
var _args = urlparser(window.location.search)
if (_args.code){
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>
<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>
"""
return _html

@ -80,39 +80,40 @@ def _realpath (uri,_config) :
return _uri
def _format (uri,_config):
_layout = _config['layout']
if 'location' in _layout :
return 'api/disk/read?uri='+uri
return uri
def read (**_args):
"""
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
"""
request = _args['request'] if 'request' in _args else None
_layout = _args['config']['layout']
_uri = request.args['uri'] if request else _args['uri'] # if 'location' in _layout :
# _uri = os.sep.join([_layout['location'],_uri])
_uri = _realpath(_uri, _args['config'])
_mimeType = 'text/plain'
# def _format (uri,_config):
# _layout = _config['layout']
# if 'location' in _layout :
# return 'api/disk/read?uri='+uri
# return uri
# def read (**_args):
# """
# 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
# """
# request = _args['request'] if 'request' in _args else None
# _layout = _args['config']['layout']
# _uri = request.args['uri'] if request else _args['uri'] # if 'location' in _layout :
# # _uri = os.sep.join([_layout['location'],_uri])
# _uri = _realpath(_uri, _args['config'])
# _mimeType = 'text/plain'
# _stream = None
# if os.path.exists(_uri):
# f = open(_uri,mode='rb')
# _stream = f.read()
# f.close()
# #
# # Inferring the type of the data to be returned
# _mimeType = 'application/octet-stream'
# _extension = _uri.split('.')[-1]
# if _extension in ['css','js','csv','html'] :
# _mimeType = f'text/{_extension}'
# if _extension == 'js' :
# _mimeType = 'text/javascript'
# elif _extension in ['png','jpg','jpeg'] :
# _mimeType = f'image/{_extension}'
if os.path.exists(_uri):
f = open(_uri,mode='rb')
_stream = f.read()
f.close()
#
# Inferring the type of the data to be returned
_mimeType = 'application/octet-stream'
_extension = _uri.split('.')[-1]
if _extension in ['css','js','csv','html'] :
_mimeType = f'text/{_extension}'
if _extension == 'js' :
_mimeType = 'text/javascript'
elif _extension in ['png','jpg','jpeg'] :
_mimeType = f'image/{_extension}'
return _stream, _mimeType
return None,_mimeType
# return _stream, _mimeType
# return None,_mimeType
def exists(**_args):
_path = _realpath(_args['uri'],_args['config'])
@ -133,12 +134,15 @@ def html(_uri,_config) :
_layout = _config['layout']
if 'location' in _layout :
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:
_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 :
# _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 = _html.replace(f'{os.sep}{_layout["root"]}',_layout['root'])
@ -154,10 +158,11 @@ def plugins (**_args):
"""
_context = _args['context']
if 'path' not in _args :
key = 'api/disk/read'
if _context :
key = f'{_context}/{key}'
return {key:read}
# key = 'api/disk/read'
# if _context :
# key = f'{_context}/{key}'
# return {key:read}
return None
_path = _args['path'] #os.sep.join([_args['root'],'plugin'])
loader = plugin_ix.Loader(file=_path)

@ -9,377 +9,3 @@
# import importlib
# import importlib.util
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
#
# 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 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)
# 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
def authorizationURL (**_args):
# _config = _args['config']
_source = _args['config']['system']['source']

@ -1,8 +1,5 @@
__doc__ = """
arguments :
--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
from flask import Flask,render_template,make_response,request, redirect, Response, session
import flask
#import transport
#from transport import providers
@ -19,7 +16,7 @@ import typer
from typing_extensions import Annotated
from typing import Optional
import numpy as np
import pandas as pd
import uuid
import datetime
@ -31,27 +28,27 @@ import cms.sites
_app = Flask(__name__)
cli = typer.Typer()
def _getHandler (app_id,resource=None) :
global _qcms
_id = _getId(app_id,resource)
# def _getHandler (app_id,resource=None) :
# global _qcms
# _id = _getId(app_id,resource)
return _qcms._apps[_id]
def _getId(app_id,app_x):
if app_x not in [None,''] :
_uri = '/'.join([app_id,app_x])
return _uri[:-1] if _uri.endswith('/') else _uri
return app_id
def _setHandler (app_id,resource) :
session['app_id'] = _getId(app_id,resource)
# return _qcms._apps[_id]
# def _getId(app_id,app_x):
# if app_x not in [None,''] :
# _uri = '/'.join([app_id,app_x])
# return _uri[:-1] if _uri.endswith('/') else _uri
# return app_id
# def _setHandler (app_id,resource) :
# session['app_id'] = _getId(app_id,resource)
@_app.route("/favicon.ico",defaults={'app':None})
@_app.route("/<app>/favicon.ico")
def favicon (app):
global _qcms
_site = _qcms.get (app)
return _read(app,None,_site.get('system.icon'))
return
_site = _qcms.get (request)
# return _read(app,None,_site.get('system.icon'))
return redirect(_site.get('system.icon')[1:]) #_site.read(request,uri=_site.get('system.icon'))
@_app.route("/<_id>/robots.txt")
@_app.route("/robots.txt",defaults={'_id':None})
def robots_txt(_id):
@ -60,7 +57,7 @@ def robots_txt(_id):
menu options
"""
global _qcms
_site = _qcms.get (_id)
_site = _qcms.get (request)
_routes = _site.get('system.routes')
_context = _site.get('system.context')
_info = [f'''
@ -78,84 +75,123 @@ def robots_txt(_id):
# return '\n'.join(_info),200,{'Content-Type':'plain/text'}
return Response('\n'.join(_info), mimetype='text/plain')
def _getIndex (app_id ,resource=None):
_handler = _getHandler(app_id,resource)
_layout = _handler.layout()
_status_code = 200
global _qcms
_index_page = 'index.html'
_args = {}
@_app.before_request
def format():
global _qcms
isroute = _qcms.inspect.isroute(request)
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)
try :
_uri = os.sep.join([_layout['root'],_layout['index']])
_id = _getId(app_id,resource)
_args = _qcms.render(_uri,'index',_id)
if not isendpoint and not isfile and not isapi and not request.path.endswith('/'):
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')
return redirect(request.path+'/')
@_app.route("/login",methods=['GET'],strict_slashes = False)
def login ():
global _qcms
_app = _qcms.get(None)
_uri = os.sep.join([_app.get('layout.root'),_app.get('layout.index')])
_html = _qcms.render(_uri,'index')
# _secureMethod = request.headers.get('method','pam')
#
# are we using a default html document ?
#
_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']
return render_template('index.html',**_html),200 #render_template('index.html',**_qcms.render(_uri,'index')),200
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("/<app>/<resource>")
@_app.route("/<app>",defaults={'resource':None})
def _altIndex(app,resource):
@_app.route("/login",methods=['POST'],strict_slashes = False)
def authenticate () :
#
# we will forward this to the appropriate agent
#
global _qcms
_id = _getId(app,resource)
_site = _qcms.get(_id)
_uri = os.sep.join([_site.get('layout.root'),_site.get('layout.index')])
return render_template('index.html',**_qcms.render(_uri,'index',_id)),200
@_app.route('/files/<path:file>',defaults={'app':None,'module':None})
@_app.route('/<app>/files/<path:file>', defaults=[{'module':None}])
@_app.route('/<app>/<module>/files/<path:file>')
def _read(app,module,file):
global _qcms
_id = _getId(app,module)
_site = _qcms.get(_id)
_stream,_mimeType = _site.read(file)
_site = _qcms.get()
_page = session.get('redirect','')
_uri = f"{_site.get('system.context')}/{_page}".replace("//","/")
if _uri.endswith('/') :
_uri = _uri[:-1]
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
return io.BytesIO(_stream),200,{'Content-Type':_mimeType}
@_app.route('/<app>/dialog')
@_app.route('/dialog',defaults={'app':None})
def _getdialog(app):
@_app.route("/logout",methods=["POST",'GET'],strict_slashes = False)
def logout():
global _qcms
_site = _qcms.get()
_id = _site.secure._authContext #-- cookie key
resp = make_response(redirect("/"))
resp.delete_cookie(_id)
return resp
@_app.route('/dialog',defaults={'app':None},methods=['POST','GET'], strict_slashes = False)
@_app.route('/<path:app>/dialog', methods=['POST','GET'], strict_slashes = False)
def _dialog(app):
global _qcms
_site = _qcms.get(app)
# _site = _qcms.get(app)
_site = _qcms.get(request)
_uri = request.headers['uri']
_id = request.headers['dom']
# _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
return render_template('dialog.html',**_args) #title=_id,html=_html)
@_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>")
@_app.route("/reload/<key>",strict_slashes = False)
def _reload(key) :
global _qcms
@ -167,10 +203,53 @@ def _reload(key) :
else:
return "",403
@_app.route('/reload',methods=['POST'])
@_app.route('/reload/',methods=['POST'],strict_slashes = False)
def reload():
_key = request.headers['key'] if 'key' in request.headers else None
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})
@ -244,3 +323,4 @@ def _help() :
pass
if __name__ == '__main__' :
cli()

@ -1,5 +1,5 @@
__author__ = "Steve L. Nyemba"
__version__= "2.4.0"
__version__= "2.4.2"
__email__ = "steve@the-phi.com"
__license__="""
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 import disk, cloud
import cms
import copy
import json
import io
import os
from jinja2 import Environment, BaseLoader, FileSystemLoader
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:
"""
@ -18,9 +49,12 @@ class IOConfig:
def __init__(self,**_args):
self._config = {'system':{},'layout':{},'plugins':{}}
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 = []
def get(self,_key) :
#
# @TODO: Include elements to be skipped (just in case & for security reasons)
if not _key :
return self._config
_keys = _key.split('.') if '.' in _key else [_key]
@ -34,6 +68,11 @@ class IOConfig:
return _object
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 :
self._config[_expr] = value
else:
@ -62,7 +101,16 @@ class Initialization (IOConfig):
#
# Invoke initialization
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):
_args = self._args
self._config = self.read_config(**_args)
@ -79,7 +127,9 @@ class Initialization (IOConfig):
self.context(**_args)
self.menu()
self.plugins()
self.plugins()
def read_config(self,**_args) :
_config = {}
if 'path' in _args :
@ -104,7 +154,9 @@ class Initialization (IOConfig):
pass
_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 :
#
# 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
_iconURI = '/'.join(["",_parentContext,self._caller.get('system.icon')])
# print ([self._caller.get('system.context'), _parentContext,_context])
if self._caller.get('system.context') != '':
_parentContext = "/"+_parentContext
_context = "/"+_context
@ -162,13 +213,14 @@ class Initialization (IOConfig):
_context = self.get('system.context')
_logo = self.get('system.logo')
_root = self.get('layout.root')
if self.get('system.source.id') == 'cloud' :
_icon = f'{_context}/api/cloud/download?doc={_logo}'
else:
_icon = f'{_context}/api/disk/read?uri={_logo}'
# _root = f'{_context}/api/disk/read?uri={_root}'
# self.set('layout.root',_root)
# if self.get('system.source.id') == 'cloud' :
# _icon = f'{_context}/api/cloud/download?doc={_logo}'
# else:
# _icon = f'{_context}/api/disk/read?uri={_logo}'
_icon = f'{_context}/{_logo}'.replace(_root,'')
self.set('system.icon',_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
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):
if self.get('layout.order.menu') :
_sorted = {}
@ -269,12 +317,14 @@ class Initialization (IOConfig):
_parentContext = self.get('system.parentContext')
_map = {}
_plugins = {}
if self.get('system.source.id') == 'cloud' :
_plugins = cloud.plugins(_context)
else:
_plugins = disk.plugins(context=_context)
_uri = 'api/system/debug'
_uri = _uri if not _context else f'{_context}/{_uri}'
# if self.get('system.source.id') == 'cloud' :
# _plugins = cloud.plugins(_context)
# else:
# _plugins = disk.plugins(context=_context)
_uri = f'{_context}/api/system/debug'
if _uri.startswith('/') :
_uri = _uri[1:]
# _uri = _uri if not _context else f'{_context}/{_uri}'
_plugins[_uri] = self.debug
if os.path.exists(_folder) and self.get('plugins'):
@ -289,6 +339,9 @@ class Initialization (IOConfig):
if _pointer :
_uri = f"api/{_filename}/{_module}"
_uri = f"{_context}/{_uri}" if _context else _uri
if (_uri.startswith("/")) :
_uri = _uri[1:]
_map[_uri] = _pointer
if _parentContext :
# _uri = f"{_parentContext}/{_context}"
@ -306,7 +359,9 @@ class Initialization (IOConfig):
# Updating plugins from disk/cloud
_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.log(action='init.plugins',module='plugins',input=list(_plugins.keys()))
@ -314,39 +369,274 @@ class Site(Initialization) :
def __init__(self,**_args):
super().__init__(**_args)
self._config['system']['portal'] = (self.get('system.routes')) == None
def html(self,_uri,_id) :
_handler = cloud if self.get('system.source.id') == 'cloud' else disk
_html = _handler.html(_uri, self.get(None))
return " ".join([f'<div id="{_id}"> ',_html,"</div>"])
def read(self,_uri) :
self._routes = []
if self.get('system.routes') :
self._routes = list(self.get('system.routes').keys())
if self._caller :
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
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:
def __init__(self,**_args):
_app = Site(**_args)
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') :
_routes = _app.get('system.routes')
for _name in _routes :
self._routes.append(_name)
_path = _routes[_name]['path']
self._sites[_name] = Site(context=_name,path=_path,caller=_app)
def render(self,_uri,_id,_appid=None):
_site = self._sites[_appid] if _appid else self._sites[self._id]
_args = {'layout':_site.get('layout')}
_system = _site.get('system')
for k in ['source','app'] :
if k in _system :
del _system[k]
_args['system'] = _system
_html = _site.html(_uri,_id)
# self._sites[f'{_name}/'] = self._sites[_name]
_env = Environment(loader=BaseLoader()).from_string(_html)
_args[_id] = str(_env.render(**_args))
return _args
self.inspect = RequestController(self._routes)
def _render(self,request):
_site = self.get(request)
_args = _site.render(request=request,id='index')
return render_template('index.html',**_args)
def _read(self,request):
_site = self.get(request)
return _site.read(request)
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):
self._id = _id
def get(self,_id=None):
return self._sites[self._id] if not _id else self._sites[_id]
def allow(self,id, request) :
_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()
http.setHeader('dom',_domId)
http.setHeader('uri',uri)
http.post(`${qcms.context}/page`,(x)=>{
http.post(`${qcms.context}/${uri}`,(x)=>{
//
// @TODO: In case of an error
var _dom = $(x.responseText)
if ( $(_dom).attr("id") == null){
$(_dom).attr("id",_domId)
}
if($(`${_parentId} #${_domId}`).length){
$(`${_parentId} #${_domId}`).remove()
}
@ -59,6 +63,7 @@ qcms.menu.Basic = function (_layout,_outputId,_domId){
// var _finalize = this._finalize
// var _domId = this._domId
var _me = this ;
_items.forEach(_item=>{
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)
_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 _menuItem = jx.dom.get.instance('DIV')
@ -126,7 +132,7 @@ qcms.menu.Basic = function (_layout,_outputId,_domId){
_menuItem.appendChild(_sub)
_domId = (_domId == null || $(_domId).length == 0)?'.main .menu' : _domId
$(`${_domId}`).append(_menuItem)
}
})
}

@ -5,7 +5,12 @@ qcms.page = {}
qcms.page.Observer = function (_parentId,_domId,_uri){
this._parentId = _parentId == null?'':_parentId
this._domId = _domId
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){
var _script = $(`${_id} script`)
@ -26,11 +31,12 @@ qcms.page.Observer = function (_parentId,_domId,_uri){
var http = HttpClient.instance()
http.setHeader('dom',this._domId)
http.setHeader('uri',this._uri)
var _uri = this._uri
http.post(`${qcms.context}/page`,function(x){
// http.setHeader('uri',this._uri)
var _uri = `${qcms.context}/${this._uri}`
http.post(_uri,function(x){
var _dom = $(x.responseText)
var _found = qcms.html.hasNode( $(_id), $(_dom))
if (_found == 0){
@ -51,7 +57,7 @@ qcms.page.Observer = function (_parentId,_domId,_uri){
}
qcms.page.loader = function(_parentId,_layout){
if (_parentId.constructor == Object) {
_layout = _parentId
_parentId = null

@ -23,18 +23,8 @@ Vanderbilt University Medical Center
<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">
<link href="{{system.context}}/static/css/themes/{{system.theme}}" rel="stylesheet" type="text/css">
<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>
{%include "libs.html" %}
</head>
<style>
.dialog {
@ -42,30 +32,38 @@ Vanderbilt University Medical Center
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(_html)
jx.modal.show({id:'foo', html:_html})
})
</script>
<body>
<div class="main">
<div class="header">
{%include "header.html" %}
<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>
<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>
<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;">404</div>
<div>Page Not found</div>
</div>
</div>
<p></p>
<div class="border-top" style="padding:8px">
<div>Page Not Found or Content Unavailable</div>
</div>
</div>
</div>
</body></html>
</p>
</div>
</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>
<html lang="en">
<head >
{% include "libs.html"%}
<title>{{layout.header.title}}</title>
<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="keywords" content="quick cms, cms, python, flask, qcms">
<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>
<script>
// sessionStorage.setItem('{{system.id}}','{{system.context|safe}}')
@ -82,8 +34,7 @@ Vanderbilt University Medical Center
// var qcms = {}
// }
qcms.context = '{{system.context}}'
// dialog.context = '{{system.context}}'
//sessionStorage.setItem('{{system.id}}','{{system.context|safe}}')
qcms.root = '{{layout.root}}'
$(document).ready( function(){
bootup.init('{{system.id}}',_layout)
@ -100,9 +51,9 @@ Vanderbilt University Medical Center
{%include "header.html" %}
</div>
<div id="menu" class="menu">
{%include "menu.html" %}
</div>
<div id="menu" class="menu">{%include "menu.html" %}</div>
<div id="content" class="content">
{%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>

@ -13,12 +13,12 @@ readme = "README.md"
license = {text = "LICENSE"}
keywords = ["cms", "www", "https", "flask", "data-transport"]
classifiers = [
"License :: OSI Approved :: MIT License",
"License :: OSI Approved :: MIT License",
"Topic :: Utilities",
]
dependencies = [
"flask",
"gitpython",
"gitpython", "flask-jwt-extended",
"termcolor",
"flask-session",
"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]
include-package-data = true
zip-safe = false
script-files = ["bin/qcms"]
script-files = ["bin/qcms","bin/qcms.bat"]
[tool.setuptools.packages.find]
include = ["meta","meta.*", "cms", "cms.*"]
include = ["cms", "cms.*"]
[tool.setuptools.dynamic]
version = {attr = "cms.meta.__version__"}

Loading…
Cancel
Save