140 lines
4.5 KiB
Python
140 lines
4.5 KiB
Python
import io
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
from dotenv import dotenv_values
|
|
from flask import Flask
|
|
from flask_session import Session
|
|
from grung.db import GrungDB
|
|
from tinydb import where
|
|
from tinydb.storages import MemoryStorage
|
|
|
|
from ttfrog import schema
|
|
from ttfrog.exceptions import ApplicationNotInitializedError
|
|
|
|
|
|
class ApplicationContext:
|
|
"""
|
|
The global context for the application, this class provides access to the Flask app instance, the GrungDB instance,
|
|
and the loaded configuration.
|
|
|
|
To prevent multiple contexts from being created, the class is instantiated at import time and replaces the module in
|
|
the symbol table. The first time it is imported, callers should call both .load_config() and .initialize(); this is
|
|
typically done at program start.
|
|
|
|
After being intialized, callers can import ttfrog.app and interact with the ApplicationContext instance directly:
|
|
|
|
>>> from ttfrog import app
|
|
>>> print(app.config.NAME)
|
|
ttfrog
|
|
"""
|
|
|
|
CONFIG_DEFAULTS = """
|
|
# ttfrog Defaults
|
|
|
|
NAME=ttfrog
|
|
LOG_LEVEL=INFO
|
|
SECRET_KEY=fnord
|
|
IN_MEMORY_DB=
|
|
|
|
DATA_ROOT=~/.dnd/ttfrog/
|
|
|
|
ADMIN_USERNAME=admin
|
|
ADMIN_EMAIL=admin@telisar
|
|
|
|
THEME=default
|
|
|
|
VIEW_URI=/
|
|
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.config: SimpleNamespace = None
|
|
self.web: Flask = None
|
|
self.db: GrungDB = None
|
|
self._initialized = False
|
|
|
|
def load_config(self, defaults: Path | None = Path("~/.dnd/ttfrog/defaults"), **overrides) -> None:
|
|
"""
|
|
Load the user configuration from the following in sources, in order:
|
|
|
|
1. ApplicationContext.CONFIG_DEFAULTS
|
|
2. The user's configuration defaults file, if any
|
|
3. Overrides specified by the caller, if any
|
|
|
|
Once the configuration is loaded, the path attribute is also configured.
|
|
"""
|
|
config_file = defaults.expanduser() if defaults else None
|
|
self.config = SimpleNamespace(
|
|
**{
|
|
**dotenv_values(stream=io.StringIO(ApplicationContext.CONFIG_DEFAULTS)),
|
|
**(dotenv_values(config_file) if config_file else {}),
|
|
**overrides,
|
|
}
|
|
)
|
|
|
|
data_root = Path(self.config.DATA_ROOT).expanduser()
|
|
self.path = SimpleNamespace(
|
|
config=config_file,
|
|
data_root=data_root,
|
|
database=data_root / f"{self.config.NAME}.json",
|
|
sessions=data_root / "session_cache",
|
|
)
|
|
|
|
def initialize(self, db: GrungDB = None, force: bool = False) -> None:
|
|
"""
|
|
Instantiate both the database and the flask application.
|
|
"""
|
|
if force or not self._initialized:
|
|
if db:
|
|
self.db = db
|
|
elif self.config.IN_MEMORY_DB:
|
|
self.db = GrungDB.with_schema(schema, storage=MemoryStorage)
|
|
else:
|
|
self.db = GrungDB.with_schema(
|
|
schema, self.path.database, sort_keys=True, indent=4, separators=(",", ": ")
|
|
)
|
|
|
|
self.theme = Path(__file__).parent / "themes" / "default"
|
|
|
|
self.web = Flask(self.config.NAME, template_folder=self.theme)
|
|
self.web.config["SECRET_KEY"] = self.config.SECRET_KEY
|
|
self.web.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0
|
|
self.web.config["DEBUG"] = True
|
|
self.web.config["SESSION_TYPE"] = "filesystem"
|
|
self.web.config["SESSION_REFRESH_EACH_REQUEST"] = True
|
|
self.web.config["SESSION_FILE_DIR"] = self.path.sessions
|
|
Session(self.web)
|
|
|
|
self._initialized = True
|
|
|
|
def check_state(self) -> None:
|
|
if not self._initialized:
|
|
raise ApplicationNotInitializedError("This action requires the application to be initialized.")
|
|
|
|
def authenticate(self, username: str, password: str) -> schema.User:
|
|
"""
|
|
Returns the User record matching the given username and password
|
|
"""
|
|
if not (username and password):
|
|
self.web.logger.debug("Need both username and password to login")
|
|
return None
|
|
|
|
user = self.db.User.get(where("name") == username)
|
|
if not user:
|
|
self.web.logger.debug(f"No user matching {username}")
|
|
return None
|
|
|
|
if not user.check_credentials(username, password):
|
|
self.web.logger.debug(f"Invalid credentials for {username}")
|
|
return None
|
|
|
|
return user
|
|
|
|
def authorize(self, user, record, requested):
|
|
return user.has_permission(record, requested)
|
|
|
|
|
|
sys.modules[__name__] = ApplicationContext()
|