diff --git a/parameciofast/console.py b/parameciofast/console.py new file mode 100644 index 0000000..47dd77c --- /dev/null +++ b/parameciofast/console.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python3 + +""" +Parameciofast is a series of wrappers for FastApi, mako and others and construct a simple headless cms. + +Copyright (C) 2025 Antonio de la Rosa Caballero + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +""" + +import argparse +import os +import shutil +import getpass +import re +from pathlib import Path +from base64 import b64encode +from parameciofast.libraries.db.webmodel import WebModel +from parameciofast.modules.fastadmin.models.admin import UserAdmin, LoginTries +from subprocess import call +from urllib.parse import urlparse + +def start(): + + parser=argparse.ArgumentParser(prog='parameciofast', description='A tool for create new paramecio2 sites') + + parser.add_argument('--path', help='The path where the paramecio site is located', required=True) + + parser.add_argument('--modules', help='A list separated by commas with the git repos for download modules for this site', required=False) + + parser.add_argument('--symlink', help='Set if create direct symlink to paramecio in new site', action='store_true') + + #parser.add_argument('--tests', help='Create a symlink to tests for check into paramecio site', action='store_true') + + # Options for deploy + + parser.add_argument('--url', help='The http/https base url of the real proxy server. Example: https://www.exampledomain.com, default is http://localhost:5000', required=False) + + parser.add_argument('--folder', help='If you deploy in a subdirectory, set it, without beggining and ending slashes', required=False) + + #parser.add_argument('--host', help='The host ip or domain where the app is binded', required=False) + + #parser.add_argument('--port', help='Change the default port 8080 to other number. Use 80 is not recommended, use 80 for the proxy server how nginx or apache', required=False) + + args=parser.parse_args() + + #print(args) + #exit(0) + + workdir=os.path.dirname(os.path.abspath(__file__)) + + # Create directory + + path=Path(args.path) + + try: + path.mkdir(0o755, True) + + except: + + print('Error: cannot create the directory. Check if exists and if you have permissions') + exit() + # Create folder settings and copy app.py, admin.py + + path_settings=args.path+'/settings' + + try: + + os.mkdir(path_settings, 0o755) + except: + print('Error: cannot create the directory. Check if exists and if you have permissions') + + # Copy the files. Need optimization, use an array for save the filenames and a simple for loop. + + try: + + shutil.copy(workdir+'/settings/config.py.sample', path_settings+'/config.py') + + except: + + print('Error: cannot copy the file config.py. Check if exists and if you have permissions for this task') + + try: + + shutil.copy(workdir+'/frontend/app.py', args.path+'/app.py') + + except: + + print('Error: cannot copy the file index.py to app.py. Check if exists and if you have permissions for this task') + + + if args.symlink==True: + try: + os.symlink(workdir, args.path+'/paramecio2', True) + + except: + print('Error: cannot symlink paramecio in new site') + + """ + if args.tests==True: + try: + os.symlink(workdir, args.path+'/paramecio2/', True) + + except: + print('Error: cannot symlink paramecio2 in new site') + """ + + with open(path_settings+'/config.py', 'r') as f: + conf=f.read() + + random_bytes = os.urandom(24) + secret_key_session = b64encode(random_bytes).decode('utf-8').strip() + + conf=conf.replace('im smoking fool', secret_key_session) + + #domain='localhost' + + #conf=conf.replace("domain='localhost'", "domain='"+args.url+"'") + """ + if args.host==None: + args.host='localhost' + + conf=conf.replace("host='localhost'", "host='"+args.host+"'") + + if args.port==None: + args.port='8080' + + + conf=conf.replace("port=8080", "port="+args.port) + """ + + base_url='/' + + if args.folder==None: + args.folder='' + else: + #args.folder='/'+args.folder + base_url='/'+args.folder+'/' + + conf=conf.replace("application_root='/'", "application_root='"+base_url+"'") + + if args.url==None: + args.url='http://localhost:5000' + + domain_url=args.url + + conf=conf.replace("domain_url='http://localhost:5000'", "domain_url='"+domain_url+"'") + + # Question about email + + e_q=input('Email for site: ') + + conf=conf.replace("no-reply@example.com", e_q) + + #if e_q=='': + + + #domain_url='http://localhost:8080' + + with open(path_settings+'/config.py', 'w') as f: + f.write(conf) + + # Question about mysql configuration? If yes, install configuration + + s=input('Do you want use paramecio with MySQL database? y/n: ') + + if s=='y' or s=='Y': + + host_db=input('MySQL database server host, by default localhost: ').strip() + + db=input('MySQL database name, by default paramecio_db: ').strip() + + user_db=input('MySQL database user, by default root: ').strip() + + pass_db=getpass.getpass('MySQL database password, by default "": ').strip() + + if host_db=='': + + host_db='localhost' + + if user_db=='': + + user_db='root' + + #user=UserAdmin() + + #Create db + + if db=="": + db='paramecio_db' + + WebModel.connections={'default': {'name': 'default', 'host': host_db, 'user': user_db, 'password': pass_db, 'db': '', 'charset': 'utf8mb4', 'set_connection': False, 'db_type': 'pymysql'} } + + connection_code="WebModel.connections={'default': {'name': 'default', 'host': '"+host_db+"', 'user': '"+user_db+"', 'password': '"+pass_db+"', 'db': '"+db+"', 'charset': 'utf8mb4', 'set_connection': False, 'db_type': 'pymysql'} }" + + with open(path_settings+'/config.py', 'a') as f: + f.write("\n\n"+connection_code) + f.close() + + sql='create database '+db + + conn=WebModel.connection() + + useradmin=UserAdmin(conn) + logintries=LoginTries(conn) + + # Check if db exists + + c=0 + + with useradmin.query('SHOW DATABASES LIKE "%s"' % db) as cur: + c=cur.rowcount + + if c==0: + useradmin.query(sql) + #print('Error: cannot create database or db doesn\'t exists, check database permissions for this user') + + #if not useradmin.query(sql): + #print('Error: cannot create database, check the data of database') + + + #else: + + useradmin.query('use '+db) + + admin=input('Do you want create admin site? y/n: ') + + if admin=='y' or admin=='Y': + + try: + + #shutil.copy(workdir+'/settings/modules.py.admin', path_settings+'/modules.py') + + #shutil.copy(workdir+'/settings/config_admin.py.sample', path_settings+'/config_admin.py') + + sql=useradmin.create_table() + + if not useradmin.query(sql): + print('Error: cannot create table admin, you can create this table with padmin.py') + else: + + sql_login=logintries.create_table() + logintries.query(sql_login) + + # Add admin module to config + with open(path_settings+'/config.py', 'r') as f: + + config_text=f.read() + + f.close() + + #apps={'welcome': ['paramecio2.modules.welcome', 'welcome_app', '/'], 'admin': ['paramecio2.modules.admin', 'admin_app', '/']} + """ + config_text=config_text.replace("apps={'welcome': ['parameciofast.modules.welcome', 'welcome_app', '/']}", "apps={'welcome': ['paramecio2.modules.welcome', 'welcome_app', '/'], 'admin': ['paramecio2.modules.admin', 'admin_app', '/']}") + + with open(path_settings+'/config.py', 'w') as f: + + f.write(config_text) + + f.close() + + try: + + shutil.copy(workdir+'/settings/modules.py.admin', path_settings+'/modules.py') + + except: + + print('Error: cannot copy the file modules.py. Check if exists and if you have permissions for this task') + """ + + print('Created admin site...') + + except: + + print('Error: cannot create the database. Check if tables exists in it and if you have permissions for this task') + exit(1) + + pass + + # Install modules + + if args.modules!=None: + + if args.modules.strip()!='': + + arr_modules=args.modules.split(',') + + final_modules=[] + + final_modules_models=[] + + if len(arr_modules)>0: + + for k, module in enumerate(arr_modules): + + module=module.strip() + + try: + + u=urlparse(module) + + module_path=os.path.basename(u.path) + + except: + print('Error: not valid url for repository') + exit(1) + + + if call("git clone %s %s/modules/%s" % (module, path, module_path), shell=True) > 0: + print('Error, cannot install the module %s' % module_path) + exit(1) + else: + print('Added module %s' % module_path) + + final_modules.append(("modules/%s" % (module_path)).replace('/', '.')) + final_modules_models.append("modules/%s" % (module_path)) + + + + # Edit config.py + + with open(path_settings+'/config.py') as f: + + modules_final='\''+'\', \''.join(final_modules)+'\'' + + p=re.compile(r"^modules=\[(.*)\]$") + + #config_file=p.sub(r"modules=[\1, "+modules_final+"]", "modules=['paramecio.modules.welcome', 'paramecio.modules.admin', 'paramecio.modules.lang', 'modules.pastafari', 'modules.monit', 'modules.example']") + + final_config='' + + for line in f: + if p.match(line): + line=p.sub(r"modules=[\1, "+modules_final+"]", line) + final_config+=line + + with open(path_settings+'/config.py', 'w') as f: + + f.write(final_config) + + print('Updated configuration for add new modules...') + + #Change workdir + + real_dir=os.getcwd() + + os.chdir(args.path) + + # Installing models + + padmin='parameciofastdb' + + os.chmod(padmin, 0o755) + + for mod_path in final_modules_models: + + models_path=mod_path+'/models' + + if os.path.isdir(models_path): + + models_files=os.listdir(models_path) + + m=re.compile(r".*\.py$") + + underscore=re.compile("^__.*") + + for f in models_files: + + if m.match(f) and not underscore.match(f): + + if call('parameciofastdb --model '+models_path+'/'+f, shell=True) > 0: + print('Error, cannot create the modules of '+models_path+'/'+f) + else: + print('Models from '+models_path+'/'+f+' created') + + # Execute two times the loop because i can need good installed models for postscript script + + # Execute postscript + + print('Executing postscripts') + + for mod_path in final_modules_models: + + postscript=mod_path+"/install/postinstall.py" + + os.chmod(padmin, 0o755) + + if os.path.isfile(postscript): + + os.chmod(postscript, 0o755) + + if call('./'+postscript, shell=True) > 0: + print('Error, cannot execute the postinstall script') + exit(1) + else: + print('Postinstall script finished') + + conn.close() + +if __name__=="__main__": + start() diff --git a/parameciofast/frontend/app.py b/parameciofast/frontend/app.py new file mode 100644 index 0000000..faf4e65 --- /dev/null +++ b/parameciofast/frontend/app.py @@ -0,0 +1,4 @@ + +from parameciofast.fast import app + + diff --git a/parameciofast/libraries/templates/modelform.phtml b/parameciofast/libraries/templates/modelform.phtml new file mode 100644 index 0000000..b6f9deb --- /dev/null +++ b/parameciofast/libraries/templates/modelform.phtml @@ -0,0 +1,28 @@ +<%def name="check_required(required)"> + % if required: + ${'*'} \ + % endif + +<%def name="help(help, name)"> + % if help: + + + % endif + +<%def name="help_tooltip(help, name)"> + % if help: + + % endif + +
+ % for form in forms.values(): + % if form.type!='hidden': +

${form.form()|n} ${form.txt_error}

+ ${help_tooltip(form.help, form.name)} + % else: + ${form.form()|n} + % endif + % endfor + ${csrf_token()|n} +
+ diff --git a/parameciofast/libraries/templates/utils/edit.phtml b/parameciofast/libraries/templates/utils/edit.phtml index ba94ffa..6c2ce05 100644 --- a/parameciofast/libraries/templates/utils/edit.phtml +++ b/parameciofast/libraries/templates/utils/edit.phtml @@ -1,18 +1,85 @@ <%inherit file="../layout.phtml"/> <%block name="content"> -
+ ${edit_form|n}

+
+
<%block name="jscript_block"> + diff --git a/parameciofast/modules/fastadmin/__init__.py b/parameciofast/modules/fastadmin/__init__.py index fe0f601..eaf7494 100644 --- a/parameciofast/modules/fastadmin/__init__.py +++ b/parameciofast/modules/fastadmin/__init__.py @@ -1,52 +1,6 @@ -from fastapi import FastAPI, Cookie, Request -from fastapi.responses import HTMLResponse, RedirectResponse -from typing import Annotated -from settings import config -#from parameciofast.fast import app -from parameciofast.libraries.session import ParamecioSession -from starlette.middleware.sessions import SessionMiddleware -from parameciofast.libraries.db.webmodel import WebModel -#from parameciofast.fast import app - -cookie_name='paramecio_session' - -if hasattr(config, 'cookie_name'): - cookie_name=config.cookie_name +from fastapi import FastAPI #print(config.apps['admin']) -url_app=config.apps['admin'][2] admin_app=FastAPI(docs_url="/docs", openapi_url="/docs/openapi.json", title='Paramecio Admin', version='0.9') -# The plugins are added inverse. - -@admin_app.middleware('http') -async def db_connect(request: Request, call_next): - - db=WebModel.connection() - - request.state.db=db - - response = await call_next(request) - - db.close() - - return response - - -# FastAPI set the middlewares in reversed order. - -@admin_app.middleware("http") -async def check_session(request: Request, call_next): - - path=request.scope['path'] - - if not request.session.get('login_admin', None) and path!=url_app+'login' and path!=url_app+'signup' and path!=url_app+'logout': - - return RedirectResponse(request.url_for('login_admin')) - - response = await call_next(request) - - return response - -admin_app.add_middleware(SessionMiddleware, secret_key=config.secret_key, max_age=None, session_cookie=cookie_name, path=url_app) diff --git a/parameciofast/modules/fastadmin/app.py b/parameciofast/modules/fastadmin/app.py index ada318e..fa45381 100644 --- a/parameciofast/modules/fastadmin/app.py +++ b/parameciofast/modules/fastadmin/app.py @@ -14,12 +14,17 @@ from settings import config from parameciofast.libraries.datetime import now, format_local_strtime, timestamp_to_datetime, obtain_timestamp from parameciofast.libraries.keyutils import create_key_encrypt, create_key from time import time +from parameciofast.libraries.session import ParamecioSession +from starlette.middleware.sessions import SessionMiddleware +from parameciofast.libraries.db.webmodel import WebModel env=env_theme(__file__) t=PTemplate(env) usermodel=UserAdmin() +url_app=config.apps['admin'][2] + login_tries=5 if hasattr(config, 'login_tries'): @@ -35,6 +40,44 @@ cookie_name='paramecio_session' if hasattr(config, 'cookie_name'): cookie_name=config.cookie_name +cookie_name='paramecio_session' + +if hasattr(config, 'cookie_name'): + cookie_name=config.cookie_name + +# The plugins are added inverse. + +@admin_app.middleware('http') +async def db_connect(request: Request, call_next): + + db=WebModel.connection() + + request.state.db=db + + response = await call_next(request) + + db.close() + + return response + + +# FastAPI set the middlewares in reversed order. + +@admin_app.middleware("http") +async def check_session(request: Request, call_next): + + path=request.scope['path'] + + if not request.session.get('login_admin', None) and path!=url_app+'login' and path!=url_app+'signup' and path!=url_app+'logout': + + return RedirectResponse(request.url_for('login_admin')) + + response = await call_next(request) + + return response + +admin_app.add_middleware(SessionMiddleware, secret_key=config.secret_key, max_age=None, session_cookie=cookie_name, path=url_app) + @admin_app.get('/', response_class=HTMLResponse) def home_admin(request: Request, paramecio_session: Annotated[str | None, Cookie(description='Cookie for validate into the admin site. The cookie name can change in you settings/config.py')] = None, remote_address: Annotated[str | None, Header()] = None): @@ -42,6 +85,7 @@ def home_admin(request: Request, paramecio_session: Annotated[str | None, Cookie return t.load_template('layout.phtml', title=i18n.tlang('Admin'), tlang=i18n.tlang, url_for=app.url_path_for, module_selected='home_admin', session=request.session) +# Begin the game @admin_app.get('/login', response_class=HTMLResponse) def login_admin(request: Request): diff --git a/parameciofast/modules/fastadmin/ausers.py b/parameciofast/modules/fastadmin/ausers.py index 9204d3d..30ce4c3 100644 --- a/parameciofast/modules/fastadmin/ausers.py +++ b/parameciofast/modules/fastadmin/ausers.py @@ -12,6 +12,7 @@ from pydantic import BaseModel from parameciofast.libraries.lists import SimpleList from parameciofast.libraries.formsutils import show_form from parameciofast.libraries.db.coreforms import SelectForm +from parameciofast.libraries.db.coreforms import PasswordForm env=env_theme(__file__) @@ -74,10 +75,21 @@ def fastadmin_users_edit(item_id: int, request: Request): forms={k:v for k,v in users.forms.items() if k in fields} + #forms['repeat_password']= + edit_form=show_form(arr_user, forms, t, session, yes_error=False, pass_values=True, modelform_tpl='forms/modelform.phtml') - return t.load_template('utils/edit.phtml', title=i18n.tlang('Edit admin users'), tlang=i18n.tlang, url_for=app.url_path_for, module_selected='fastadmin_users', session=request.session, edit_form=edit_form) + url_edit=request.url_for('fastadmin_users_post', item_id=item_id) + + return t.load_template('utils/edit.phtml', title=i18n.tlang('Edit admin users'), tlang=i18n.tlang, url_for=app.url_path_for, module_selected='fastadmin_users', session=request.session, edit_form=edit_form, url_edit=url_edit) #return "" +@admin_app.post('/ausers/edit/{item_id}') +def fastadmin_users_post(item_id: int, request: Request): + error=1 + + txt_message='' + + return {'error': error, 'txt_message': txt_message} diff --git a/parameciofast/settings/config.py.sample b/parameciofast/settings/config.py.sample new file mode 100644 index 0000000..ba18dac --- /dev/null +++ b/parameciofast/settings/config.py.sample @@ -0,0 +1,45 @@ +from parameciofast.libraries.db.webmodel import WebModel +from parameciofast.libraries.i18n import I18n + +from parameciofast.libraries.db.databases.sqlalchemy import SqlClass + +SqlClass.disable_pool=True + +I18n.dict_i18n=['en-US'] + +secret_key="hPefRWISrJIY6DBZwCEj2Nyuhp7RTNUI" + +static_folder='/media/default' + +static_url_path='/media/default' + +cookie_name='paramecio_session' + +# The second parameter is the prefix if you want use more applications. + +#apps={'admin': ['parameciofast.modules.fastadmin', 'admin_app', '/admin/']} + +#apps={'admin': ['modules.bot', 'bot_app', '/apibot/']} + +default_module='welcome' + +theme="default" + +reloader=True + +application_root='/' + +yes_static=True + +domain_url='http://localhost:8000' + +#media_folder='/media/' + +media_url='/media/' + +portal_email="antonio.delarosa@salirdelhoyo.com" + + +#Db configuration + +WebModel.connections={'default': {'name': 'default', 'host': 'localhost', 'user': 'root', 'password': 'sirena', 'db': 'catalog_db', 'charset': 'utf8mb4', 'set_connection': False, 'db_type': 'pymysql'} }