s
This commit is contained in:
parent
df721042ac
commit
445fc5fe9a
1
examples/heroweb_example_base/css
Symbolic link
1
examples/heroweb_example_base/css
Symbolic link
@ -0,0 +1 @@
|
||||
../../herowebserver/static/css
|
1
examples/heroweb_example_base/js
Symbolic link
1
examples/heroweb_example_base/js
Symbolic link
@ -0,0 +1 @@
|
||||
../../herowebserver/static/js
|
@ -1,22 +1,51 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from fastapi import FastAPI, Form, HTTPException, Request
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import (
|
||||
FileResponse,
|
||||
HTMLResponse,
|
||||
JSONResponse,
|
||||
RedirectResponse,
|
||||
)
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi_mail import ConnectionConfig, FastMail, MessageSchema, MessageType
|
||||
from infoserver.db import DB
|
||||
from infoserver.dbmem import DBMem
|
||||
from infoserver.model import ACE, InfoPointer, ObjNotFound
|
||||
from infoserver.dependencies import Dependencies
|
||||
from infoserver.routers.router_index import router_index
|
||||
from infoserver.routers.router_login import router_login
|
||||
from infoserver.routers.router_pdf_preso import router_pdf
|
||||
from infoserver.routers.router_static import router_static
|
||||
from infoserver.routers.router_template import router_template
|
||||
from jwt.exceptions import PyJWTError
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from web.auth import JWTHandler
|
||||
|
||||
# Initialize FastAPI app
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Set your paths here
|
||||
DB_PATH = '~/code/git.ourworld.tf/freeflowuniverse/heroweb/authdb_example'
|
||||
TEMPLATES_DIR = '~/code/git.ourworld.tf/freeflowuniverse/heroweb/poc/out'
|
||||
STATIC_DIR = '~/code/git.ourworld.tf/freeflowuniverse/heroweb/poc/static'
|
||||
HEROWEB_DIR = '~/code/git.ourworld.tf/tfgrid/info_tfgrid/heroweb'
|
||||
COLLECTIONS_DIR = '~/hero/var/collections'
|
||||
|
||||
DB_PATH = os.path.abspath(os.path.expanduser(DB_PATH))
|
||||
TEMPLATES_DIR = os.path.abspath(os.path.expanduser(TEMPLATES_DIR))
|
||||
STATIC_DIR = os.path.abspath(os.path.expanduser(STATIC_DIR))
|
||||
STATIC_DIR2 = os.path.abspath(os.path.join(os.path.dirname(__file__), 'static'))
|
||||
HEROWEB_DIR = os.path.abspath(os.path.expanduser(HEROWEB_DIR))
|
||||
COLLECTIONS_DIR = os.path.abspath(os.path.expanduser(COLLECTIONS_DIR))
|
||||
|
||||
SERVERHOST = 'http://localhost:8000'
|
||||
|
||||
if not os.path.exists(DB_PATH):
|
||||
raise FileNotFoundError(f'Database path does not exist: {DB_PATH}')
|
||||
if not os.path.exists(TEMPLATES_DIR):
|
||||
raise FileNotFoundError(
|
||||
f'Templates directory does not exist: {TEMPLATES_DIR}'
|
||||
)
|
||||
if not os.path.exists(STATIC_DIR):
|
||||
raise FileNotFoundError(f'Static directory does not exist: {STATIC_DIR}')
|
||||
if not os.path.exists(STATIC_DIR2):
|
||||
raise FileNotFoundError(f'Static directory does not exist: {STATIC_DIR}')
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Add session middleware for cookie management
|
||||
@ -25,195 +54,71 @@ if not jwt_secret_key:
|
||||
raise EnvironmentError('JWT_SECRET_KEY environment variable is not set')
|
||||
app.add_middleware(SessionMiddleware, secret_key=jwt_secret_key)
|
||||
|
||||
# Email configuration
|
||||
conf = ConnectionConfig(
|
||||
MAIL_USERNAME=os.getenv('MAIL_USERNAME'),
|
||||
MAIL_PASSWORD=os.getenv('MAIL_PASSWORD'),
|
||||
MAIL_FROM=os.getenv('MAIL_FROM'),
|
||||
MAIL_PORT=int(os.getenv('MAIL_PORT', 587)),
|
||||
MAIL_SERVER=os.getenv('MAIL_SERVER'),
|
||||
MAIL_STARTTLS=True, # This replaces MAIL_TLS
|
||||
MAIL_SSL_TLS=False, # This replaces MAIL_SSL
|
||||
USE_CREDENTIALS=True,
|
||||
# Include your routers here
|
||||
app.include_router(router_static)
|
||||
app.include_router(router_login)
|
||||
app.include_router(router_pdf)
|
||||
app.include_router(router_index)
|
||||
app.include_router(router_template)
|
||||
|
||||
deps = Dependencies(
|
||||
DB_PATH,
|
||||
TEMPLATES_DIR,
|
||||
STATIC_DIR,
|
||||
STATIC_DIR2,
|
||||
HEROWEB_DIR,
|
||||
COLLECTIONS_DIR,
|
||||
SERVERHOST,
|
||||
)
|
||||
app.deps = deps
|
||||
|
||||
# Check if all required environment variables are set
|
||||
required_env_vars = [
|
||||
'MAIL_USERNAME',
|
||||
'MAIL_PASSWORD',
|
||||
'MAIL_FROM',
|
||||
'MAIL_PORT',
|
||||
'MAIL_SERVER',
|
||||
]
|
||||
missing_vars = [var for var in required_env_vars if not os.getenv(var)]
|
||||
|
||||
if missing_vars:
|
||||
raise EnvironmentError(
|
||||
f"Missing required environment variables: {', '.join(missing_vars)}"
|
||||
)
|
||||
|
||||
# Jinja2 templates for rendering HTML
|
||||
templates = Jinja2Templates(directory='templates')
|
||||
|
||||
# Initialize JWTHandler
|
||||
jwt_handler = JWTHandler()
|
||||
|
||||
db = DB(
|
||||
'~/code/git.ourworld.tf/freeflowuniverse/heroweb/authdb_example',
|
||||
reset=False,
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=['*'], # Allows all origins
|
||||
allow_credentials=True,
|
||||
allow_methods=['*'], # Allows all methods
|
||||
allow_headers=['*'], # Allows all headers
|
||||
)
|
||||
dbmem = DBMem(db, 'kristof@incubaid.com')
|
||||
# print('get users from group moreusers')
|
||||
# print(dbmem_kristof.group_get_users('moreusers'))
|
||||
|
||||
|
||||
@app.middleware('http')
|
||||
async def check_authentication(request: Request, call_next):
|
||||
async def check_authentication(
|
||||
request: Request,
|
||||
call_next,
|
||||
):
|
||||
logger.debug(f'Received request for URL: {request.url.path}')
|
||||
|
||||
# BYPASS
|
||||
return await call_next(request)
|
||||
|
||||
if request.url.path in ['/signup', '/loginsubmit', '/register']:
|
||||
logger.debug(
|
||||
'Skipping authentication for signup, loginsubmit, or register path'
|
||||
)
|
||||
return await call_next(request)
|
||||
|
||||
token = request.cookies.get('access_token')
|
||||
if not token:
|
||||
logger.debug('No access token found, redirecting to /signup')
|
||||
return RedirectResponse(url='/signup')
|
||||
|
||||
jwt_handler = request.app.deps.jwt_handler
|
||||
try:
|
||||
email = jwt_handler.verify_access_token(token)
|
||||
except PyJWTError:
|
||||
except PyJWTError as e:
|
||||
logger.error(f'Token verification failed: {e}')
|
||||
return RedirectResponse(url='/signup')
|
||||
|
||||
request.state.email = email
|
||||
return await call_next(request)
|
||||
logger.debug(f'Authenticated user: {email}')
|
||||
|
||||
|
||||
# Step 1: Render login page to accept email
|
||||
@app.get('/', response_class=HTMLResponse)
|
||||
async def myroot(request: Request):
|
||||
return RedirectResponse(url='/signup')
|
||||
|
||||
|
||||
@app.get('/signup', response_class=HTMLResponse)
|
||||
async def login_form(request: Request):
|
||||
return templates.TemplateResponse('login.html', {'request': request})
|
||||
|
||||
|
||||
# Step 2: Handle form submission, generate token, send login email
|
||||
@app.post('/loginsubmit')
|
||||
async def send_login_email(request: Request, email: str = Form(...)):
|
||||
if not email:
|
||||
# Redirect to signup page if email is not provided
|
||||
return RedirectResponse(url='/signup')
|
||||
# Generate the access token and email link
|
||||
access_token = jwt_handler.create_access_token({'sub': email})
|
||||
email_link = f'http://verse.tf:8000/register?token={access_token}'
|
||||
|
||||
# Render the email template with the email link
|
||||
rendered_template = templates.get_template('email.html').render(
|
||||
{'email_link': email_link}
|
||||
)
|
||||
|
||||
# Create the email message
|
||||
message = MessageSchema(
|
||||
subject='Login to your account',
|
||||
recipients=[email], # List of recipient emails
|
||||
body=rendered_template, # The rendered HTML content
|
||||
subtype=MessageType.html, # Specify the subtype as HTML
|
||||
)
|
||||
|
||||
# Send the email
|
||||
fm = FastMail(conf)
|
||||
await fm.send_message(message)
|
||||
|
||||
return {'message': 'Login link has been sent to your email.'}
|
||||
|
||||
|
||||
# Step 3: Handle email link redirection and set JWT in cookies
|
||||
@app.get('/register')
|
||||
async def register(request: Request, token: str):
|
||||
try:
|
||||
jwt_handler.verify_access_token(token)
|
||||
except PyJWTError:
|
||||
raise HTTPException(status_code=401, detail='Invalid token')
|
||||
|
||||
response = RedirectResponse(url='/info')
|
||||
response.set_cookie(key='access_token', value=token)
|
||||
response = await call_next(request)
|
||||
# logger.debug(f'Response status code: {response.status_code}')
|
||||
return response
|
||||
|
||||
|
||||
# Step 4: User info page, read JWT from cookies
|
||||
@app.get('/info', response_class=HTMLResponse)
|
||||
async def get_user_info(request: Request):
|
||||
token = request.cookies.get('access_token')
|
||||
if not token:
|
||||
# raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
return RedirectResponse(url='/signup')
|
||||
if __name__ == '__main__':
|
||||
import uvicorn
|
||||
|
||||
try:
|
||||
email = jwt_handler.verify_access_token(token)
|
||||
except PyJWTError:
|
||||
# raise HTTPException(status_code=401, detail="Invalid token")
|
||||
return RedirectResponse(url='/signup')
|
||||
|
||||
return templates.TemplateResponse(
|
||||
'info.html', {'request': request, 'email': email}
|
||||
)
|
||||
|
||||
|
||||
@app.get('/{infoname}', response_class=HTMLResponse)
|
||||
async def list_dir(request: Request, infoname: str):
|
||||
try:
|
||||
info = fs_db.info_get(infoname)
|
||||
except ObjNotFound:
|
||||
raise HTTPException(status_code=404, detail='Info not found')
|
||||
|
||||
user_email = request.state.email
|
||||
try:
|
||||
user_ace = get_user_ace(info, user_email)
|
||||
# no need to check for access rights since this is reading
|
||||
|
||||
except Exception:
|
||||
raise HTTPException(status_code=401, detail='Unauthorized')
|
||||
|
||||
list_dir = os.listdir(info.path)
|
||||
return JSONResponse({'dir': list_dir})
|
||||
|
||||
|
||||
def get_user_ace(info: InfoPointer, user: str) -> ACE:
|
||||
for acl_str in info.acl:
|
||||
acl = fs_db.acl_get(acl_str)
|
||||
for entry in acl.entries:
|
||||
if entry.user == user:
|
||||
return entry
|
||||
|
||||
raise Exception(f'user {user} not authorized')
|
||||
|
||||
|
||||
@app.get('/{infoname}/{relative_path:path}', response_class=HTMLResponse)
|
||||
async def serve_file(request: Request, infoname: str, relative_path: str):
|
||||
try:
|
||||
info = fs_db.info_get(infoname)
|
||||
except ObjNotFound:
|
||||
raise HTTPException(status_code=404, detail='Info not found')
|
||||
|
||||
user_email = request.state.email
|
||||
try:
|
||||
user_ace = get_user_ace(info, user_email)
|
||||
# no need to check for access rights since this is reading
|
||||
|
||||
except Exception:
|
||||
raise HTTPException(status_code=401, detail='Unauthorized')
|
||||
|
||||
new_path = os.path.join(info.path, relative_path)
|
||||
if not os.path.exists(new_path):
|
||||
raise HTTPException(status_code=404, detail='File not found')
|
||||
|
||||
if os.path.isdir(new_path):
|
||||
list_dir = os.listdir(new_path)
|
||||
return JSONResponse({'dir': list_dir})
|
||||
|
||||
if new_path.endswith('.pdf'):
|
||||
return templates.TemplateResponse(
|
||||
'pdf_viewer.html',
|
||||
{'request': request, 'pdf_url': f'/static/{relative_path}'},
|
||||
)
|
||||
else:
|
||||
return FileResponse(new_path)
|
||||
uvicorn.run(app, host='0.0.0.0', port=8000)
|
||||
|
@ -1,3 +1,9 @@
|
||||
pushd /root/code/git.ourworld.tf/freeflowuniverse/heroweb/authserver
|
||||
#!/bin/bash
|
||||
set -e
|
||||
SERVER_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
source ~/code/git.ourworld.tf/freeflowuniverse/heroweb/myenv.sh
|
||||
fi
|
||||
pushd $SERVER_DIR
|
||||
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
||||
popd
|
||||
popd > /dev/null
|
||||
|
@ -1,7 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source ${BASE_DIR}/myenv.sh
|
||||
BASE_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
|
||||
if [ -z "$CONTEXTROOT" ]; then
|
||||
source ${BASE_DIR}/myenv.sh
|
||||
fi
|
||||
cd $BASE_DIR
|
||||
|
||||
python3 -m pip install -r "$BASE_DIR/requirements.txt"
|
||||
|
@ -1 +0,0 @@
|
||||
../../../projectmycelium/hero_server/lib
|
56
lib/webcomponents/main/model_view.py
Normal file
56
lib/webcomponents/main/model_view.py
Normal file
@ -0,0 +1,56 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class NavItem:
|
||||
href: str
|
||||
text: str
|
||||
class_name: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Navbar:
|
||||
brand: NavItem
|
||||
items: List[NavItem]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MarkdownContent:
|
||||
nav: str
|
||||
content: str
|
||||
title: str = 'MyDoc'
|
||||
|
||||
|
||||
@dataclass
|
||||
class Doc:
|
||||
navbar: Navbar
|
||||
markdown: MarkdownContent
|
||||
title: str = 'An Example Index Page'
|
||||
|
||||
|
||||
def example() -> Doc:
|
||||
import os
|
||||
|
||||
base_dir = os.path.dirname(__file__)
|
||||
templates_dir = os.path.join(base_dir, 'templates')
|
||||
|
||||
with open(os.path.join(templates_dir, 'example_main.md'), 'r') as f:
|
||||
example_main = f.read()
|
||||
|
||||
with open(os.path.join(templates_dir, 'example_nav.md'), 'r') as f:
|
||||
example_nav = f.read()
|
||||
|
||||
navbar = Navbar(
|
||||
brand=NavItem(href='#', text='MyWebsite', class_name='brand'),
|
||||
items=[
|
||||
NavItem(href='#home', text='Home'),
|
||||
NavItem(href='#about', text='About'),
|
||||
NavItem(href='#services', text='Services'),
|
||||
NavItem(href='#contact', text='Contact'),
|
||||
],
|
||||
)
|
||||
|
||||
markdown_content = MarkdownContent(nav=example_nav, content=example_main)
|
||||
|
||||
return Doc(navbar=navbar, markdown=markdown_content)
|
27
lib/webcomponents/main/render.py
Normal file
27
lib/webcomponents/main/render.py
Normal file
@ -0,0 +1,27 @@
|
||||
import os
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from webcomponents.main.model_view import example
|
||||
|
||||
|
||||
def render(timeline_id: int = 0) -> str:
|
||||
# Create an agenda instance
|
||||
|
||||
mydoc = example()
|
||||
|
||||
# Set up Jinja2 environment and load the template
|
||||
base_dir = os.path.dirname(__file__)
|
||||
env = Environment(
|
||||
loader=FileSystemLoader(searchpath=f'{base_dir}/templates')
|
||||
)
|
||||
|
||||
# pudb.set_trace()
|
||||
|
||||
template = env.get_template('index.html')
|
||||
|
||||
# Render the template with the agenda data
|
||||
output = template.render(doc=mydoc)
|
||||
|
||||
print(output)
|
||||
|
||||
return output
|
38
lib/webcomponents/main/templates/example_main.md
Normal file
38
lib/webcomponents/main/templates/example_main.md
Normal file
@ -0,0 +1,38 @@
|
||||
# Welcome to the Example
|
||||
|
||||
This is a **Markdown** example.
|
||||
|
||||
# Section 1
|
||||
Content for section 1.
|
||||
|
||||
## Section 2
|
||||
Content for section 2.
|
||||
|
||||
### Section 3
|
||||
|
||||
Content for section 3.
|
||||
|
||||
- something
|
||||
- yes
|
||||
- incredible
|
||||
|
||||
> This is a blockquote.
|
||||
|
||||
| Name | Email | Description |
|
||||
|------------|-------------------|-------------------|
|
||||
| John Doe | john@example.com | Developer |
|
||||
| Jane Smith | jane@example.com | Designer |
|
||||
| Bob Brown | bob@example.com | Manager |
|
||||
| Alice Blue | alice@example.com | Engineer |
|
||||
| Eve White | eve@example.com | Analyst |
|
||||
| Tom Black | tom@example.com | Consultant |
|
||||
|
||||
### Section 4
|
||||
|
||||
Content for section 4.
|
||||
|
||||
- something
|
||||
- yes
|
||||
- incredible
|
||||
|
||||
> This is a blockquote.
|
7
lib/webcomponents/main/templates/example_nav.md
Normal file
7
lib/webcomponents/main/templates/example_nav.md
Normal file
@ -0,0 +1,7 @@
|
||||
- [intro](#intro)
|
||||
- [products](#products)
|
||||
- [car](#car)
|
||||
- [plane](#plane)
|
||||
- [features](#features)
|
||||
- [know more](#know-more)
|
||||
- [documentation](#documentation)
|
60
lib/webcomponents/main/templates/index.html
Normal file
60
lib/webcomponents/main/templates/index.html
Normal file
@ -0,0 +1,60 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ doc.title }}</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.0.6/css/pico.classless.min.css"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/heroweb.css" />
|
||||
</head>
|
||||
<body>
|
||||
{% if doc.markdown.nav %}
|
||||
<textarea id="markdown-nav" style="display: none">
|
||||
{{ doc.markdown.nav }}
|
||||
</textarea
|
||||
>
|
||||
{% endif %}
|
||||
<!-- now the main one -->
|
||||
{% if doc.markdown.content %}
|
||||
<textarea id="markdown-input" style="display: none">
|
||||
{{ doc.markdown.content }}
|
||||
</textarea
|
||||
>
|
||||
{% endif %}
|
||||
|
||||
<nav>
|
||||
<ul>
|
||||
{% for item in doc.navbar.items %}
|
||||
<li>
|
||||
<a href="{{ item.href }}" class="{{ item.class_name }}"
|
||||
>{{ item.text }}</a
|
||||
>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<navbar-right>
|
||||
<input
|
||||
type="search"
|
||||
name="search"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
<a href="#login">Login</a>
|
||||
<div id="theme-switcher-icons"></div>
|
||||
</navbar-right>
|
||||
</ul>
|
||||
</nav>
|
||||
<main class="container">
|
||||
<mynav id="mynav"> </mynav>
|
||||
<article id="markdown-output"></article>
|
||||
<docnav>
|
||||
<br />
|
||||
<h5>{{ doc.markdown.title }}</h5>
|
||||
<ul id="content-pointers"></ul>
|
||||
</docnav>
|
||||
</main>
|
||||
<script src="/static/js/heroweb.js"></script>
|
||||
</body>
|
||||
</html>
|
11
myenv.sh
11
myenv.sh
@ -1,7 +1,6 @@
|
||||
#!/bin/bash
|
||||
SCRIPT_PATH="${BASH_SOURCE[0]:-$0}"
|
||||
export BASE_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
|
||||
#export VENV_DIR="${HOME}/.venv"
|
||||
export VENV_DIR="${BASE_DIR}/.venv"
|
||||
|
||||
python3 -m venv "$VENV_DIR"
|
||||
@ -25,4 +24,14 @@ if [ -f "$SECRET_FILE" ]; then
|
||||
source "$SECRET_FILE"
|
||||
fi
|
||||
|
||||
PYTHON_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
|
||||
|
||||
WEBLIB_PATH="${VENV_DIR}/lib/python${PYTHON_VERSION}/site-packages/weblib.pth"
|
||||
HEROLIB_PATH="${VENV_DIR}/lib/python${PYTHON_VERSION}/site-packages/herolib.pth"
|
||||
|
||||
if [ ! -f "$WEBLIB_PATH" ] || [ ! -f "$HEROLIB_PATH" ]; then
|
||||
echo "One or both of the required .pth files are missing. Running install.sh."
|
||||
source "${BASE_DIR}/install.sh"
|
||||
fi
|
||||
|
||||
echo "We're good to go"
|
||||
|
Loading…
Reference in New Issue
Block a user