app: initial version with templating

This commit is contained in:
xeals 2022-09-03 14:33:52 +10:00
parent 28b2d89b06
commit 686c9efc2c
Signed by: xeals
GPG Key ID: A498C7AF27EC6B5C
4 changed files with 215 additions and 0 deletions

56
frontpage/app.py Normal file
View File

@ -0,0 +1,56 @@
"""Main entrypoint."""
import argparse
import secrets
from flask import Flask
from flask_pyoidc import OIDCAuthentication
from flask_pyoidc.provider_configuration import ClientMetadata, ProviderConfiguration
from frontpage import config, routes
from frontpage.config import OidcConfig
app = Flask(__name__)
def _init_oidc(cfg: OidcConfig) -> OIDCAuthentication:
if cfg.client_secret_file is not None:
with open(cfg.client_secret_file, encoding="UTF-8") as f:
secret = f.read()
else:
secret = cfg.client_secret
print(secret)
client = ClientMetadata(client_id=cfg.client_id, client_secret=secret)
# Ensure we have the minimum set of scopes for things to work.
scopes = {"openid"}
scopes.update(cfg.scopes)
auth_params = {"scope": list(scopes)}
provider = ProviderConfiguration(
issuer=cfg.issuer, client_metadata=client, auth_request_params=auth_params
)
auth = OIDCAuthentication({"default": provider}, app)
return auth
def main():
"""
Entrypoint.
"""
parser = argparse.ArgumentParser()
parser.add_argument(
"-c", "--config", metavar="FILE", help="configuration file", required=True
)
args = parser.parse_args()
cfg = config.load(args.config)
auth = _init_oidc(cfg.oidc)
app.register_blueprint(routes.register(auth, "default"))
app.config.update({"DEBUG": cfg.app.debug, "SECRET_KEY": secrets.token_hex()})
app.config["user_config"] = cfg
# Reloader doesn't work on NixOS
app.run(use_reloader=False)

85
frontpage/config.py Normal file
View File

@ -0,0 +1,85 @@
"""Configuration classes and utilities."""
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional
import toml
import dacite
from flask import current_app
@dataclass
class CoreConfig:
"""Core application configuration."""
debug: bool = False
@dataclass
class OidcConfig:
"""OIDC client configuration."""
issuer: str
client_id: str
client_secret: Optional[str] = None
client_secret_file: Optional[str] = None
scopes: List[str] = field(default_factory=list)
@dataclass
class AppConfig:
"""App link configuration."""
url: str
name: Optional[str] = None
image: Optional[str] = None
description: Optional[str] = None
groups: List[str] = field(default_factory=list)
@dataclass
class Config:
"""Top-level configuration."""
app: CoreConfig
oidc: OidcConfig
apps: Dict[str, AppConfig] = field(default_factory=dict)
class ConfigError(Exception):
pass
def _validate(config: Config) -> None:
oidc = config.oidc
if oidc.client_secret is None and oidc.client_secret_file is None:
raise ConfigError(
"exactly one of oidc.client_secret or oidc.client_secret_file is required"
)
if oidc.client_secret is not None and oidc.client_secret_file is not None:
raise ConfigError(
"exactly one of oidc.client_secret or oidc.client_secret_file is required"
)
def load(file: str) -> Config:
"""
Load the configuration from a `file` path containing a TOML configuration.
:param file: a file path to a TOML config
:return: the configuration object
"""
path = Path(file)
cfg = dacite.from_dict(data_class=Config, data=toml.load(path))
_validate(cfg)
return cfg
def current_config() -> Config:
"""
Load the configuration from the current Flask app.
:return: the configuration object
"""
return current_app.config["user_config"]

40
frontpage/routes.py Normal file
View File

@ -0,0 +1,40 @@
"""Route configuration."""
from typing import Any, Iterable, List
import flask
from flask import Blueprint, current_app, render_template
from flask_pyoidc import OIDCAuthentication
from flask_pyoidc.user_session import UserSession
from frontpage.config import AppConfig, current_config
def _allowed(items_from: Iterable[Any], items_in: Iterable[Any]) -> bool:
if len(items_from) == 0:
return True
for item in items_from:
if item in items_in:
return True
return False
def register(auth: OIDCAuthentication, auth_provider: str) -> Blueprint:
routes = Blueprint("routes", __name__)
@routes.route("/")
@auth.oidc_auth(auth_provider)
def home():
"""
Renders the home route.
"""
user_session = UserSession(flask.session)
groups: List[str] = user_session.userinfo["groups"]
apps: AppConfig = current_config().apps
allowed_apps = {
ident: a for ident, a in apps.items() if _allowed(a.groups, groups)
}
return render_template("home.html", apps=allowed_apps, groups=groups)
return routes

View File

@ -0,0 +1,34 @@
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/chota@latest">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
<title>Front page</title>
</head>
<body>
<nav class="nav">
<div class="nav-left">
<a class="brand" href="/">Front page</a>
</div>
</nav>
<main class="container">
<div class="row">
{% for ident, cfg in apps.items() %}
<div class="card col-4">
<header>
<a href="{{ cfg.url }}">
{{ cfg.name or ident }}
</a>
</header>
{% if cfg.image %}
<img src="{{ cfg.image }}">
{% endif %}
{% if cfg.description %}
<p>{{ cfg.description }}</p>
{% endif %}
</div>
{% endfor %}
</div>
</main>
</body>
</html>