This commit is contained in:
kristof de spiegeleer 2024-09-14 06:59:42 +03:00
parent df721042ac
commit 445fc5fe9a
15 changed files with 296 additions and 185 deletions

View File

@ -0,0 +1 @@
../../herowebserver/static/css

View File

@ -0,0 +1 @@
../../herowebserver/static/js

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -1 +0,0 @@
../../../projectmycelium/hero_server/lib

View 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)

View 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

View 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.

View File

@ -0,0 +1,7 @@
- [intro](#intro)
- [products](#products)
- [car](#car)
- [plane](#plane)
- [features](#features)
- [know more](#know-more)
- [documentation](#documentation)

View 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>

View File

@ -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"