Compare commits

..

6 Commits

Author SHA1 Message Date
480cd4a4fe Drop copy for now
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-12 17:39:29 +02:00
c5c856e6e9 Further into invite link display 2021-06-12 02:15:50 +02:00
96e718db3a aioredis integration and forced authentication 2021-06-12 00:53:57 +02:00
1acc7705df Style changes for logout 2021-06-11 22:30:13 +02:00
30edb39163 Support logging in and out 2021-06-11 22:28:43 +02:00
5942468164 Document env vars 2021-06-11 22:28:32 +02:00
8 changed files with 254 additions and 31 deletions

View File

@ -45,5 +45,6 @@ your technology stack.
It's a [FastAPI](https://fastapi.tiangolo.com/) application. It's a [FastAPI](https://fastapi.tiangolo.com/) application.
``` ```
$ set -a && source .envrc && set +a
$ make $ make
``` ```

View File

@ -1,20 +1,37 @@
"""Community Keycloak SSO user management.""" """Community Keycloak SSO user management."""
import json
from datetime import datetime as dt
from datetime import timedelta
from os import environ from os import environ
from uuid import uuid4
import httpx
from aioredis import create_redis_pool
from authlib.integrations.starlette_client import OAuth, OAuthError from authlib.integrations.starlette_client import OAuth, OAuthError
from fastapi import FastAPI, Request from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from humanize import naturaldelta
from starlette.exceptions import HTTPException
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
APP_SECRET_KEY = environ.get("APP_SECRET_KEY") APP_SECRET_KEY = environ.get("APP_SECRET_KEY")
KEYCLOAK_CLIENT_ID = environ.get("KEYCLOAK_CLIENT_ID") KEYCLOAK_CLIENT_ID = environ.get("KEYCLOAK_CLIENT_ID")
KEYCLOAK_CLIENT_SECRET = environ.get("KEYCLOAK_CLIENT_SECRET") KEYCLOAK_CLIENT_SECRET = environ.get("KEYCLOAK_CLIENT_SECRET")
KEYCLOAK_DOMAIN = environ.get("KEYCLOAK_DOMAIN") KEYCLOAK_DOMAIN = environ.get("KEYCLOAK_DOMAIN")
KEYCLOAK_REALM = environ.get("KEYCLOAK_REALM") KEYCLOAK_REALM = environ.get("KEYCLOAK_REALM")
BASE_URL = f"https://{KEYCLOAK_DOMAIN}/auth/realms/{KEYCLOAK_REALM}/protocol/openid-connect" # noqa
app = FastAPI() REDIS_DB = environ.get("REDIS_DB")
REDIS_HOST = environ.get("REDIS_HOST")
REDIS_PORT = environ.get("REDIS_PORT")
INVITE_TIME_LIMIT = environ.get("INVITE_TIME_LIMIT")
app = FastAPI(docs_url=None, redoc_url=None)
app.add_middleware(SessionMiddleware, secret_key=APP_SECRET_KEY) app.add_middleware(SessionMiddleware, secret_key=APP_SECRET_KEY)
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
@ -24,20 +41,79 @@ oauth.register(
client_kwargs={"scope": "openid profile email"}, client_kwargs={"scope": "openid profile email"},
client_id=KEYCLOAK_CLIENT_ID, client_id=KEYCLOAK_CLIENT_ID,
client_secret=KEYCLOAK_CLIENT_SECRET, client_secret=KEYCLOAK_CLIENT_SECRET,
authorize_url=f"https://{KEYCLOAK_DOMAIN}/auth/realms/{KEYCLOAK_REALM}/protocol/openid-connect/auth", authorize_url=f"{BASE_URL}/auth",
access_token_url=f"https://{KEYCLOAK_DOMAIN}/auth/realms/{KEYCLOAK_REALM}/protocol/openid-connect/token", access_token_url=f"{BASE_URL}/token",
jwks_uri=f"https://{KEYCLOAK_DOMAIN}/auth/realms/{KEYCLOAK_REALM}/protocol/openid-connect/certs", jwks_uri=f"{BASE_URL}/certs",
) )
@app.get("/", response_class=HTMLResponse) class RequiresLoginException(Exception):
async def home(request: Request): pass
@app.exception_handler(RequiresLoginException)
async def requires_login(request, exception):
return RedirectResponse(request.url_for("login"))
@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
home = request.url_for("login")
return HTMLResponse(f"<p>{exc.detail} (<a href='{home}'>home</a>)</p>")
async def logged_in(request: Request):
user = request.session.get("user") user = request.session.get("user")
if user: if not user:
return templates.TemplateResponse( raise RequiresLoginException
"index.html", context={"request": request, "user": user} return user
)
return RedirectResponse(request.url_for("login_keycloak"))
async def get_user(request: Request):
return request.session.get("user")
async def get_invites(request: Request, user=Depends(get_user)):
username = user["preferred_username"]
invites = await app.state.redis.get(username)
if invites:
humanised = []
for invite in json.loads(invites):
invite["human_time"] = naturaldelta(
dt.fromisoformat(invite["time"])
+ timedelta(days=int(INVITE_TIME_LIMIT))
)
humanised.append(invite)
return humanised
return []
@app.on_event("startup")
async def starup_event():
app.state.redis = await create_redis_pool(
f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}?encoding=utf-8"
)
@app.on_event("shutdown")
async def shutdown_event():
app.state.redis.close()
await app.state.redis.wait_closed()
@app.get("/", dependencies=[Depends(logged_in)])
async def home(
request: Request, user=Depends(get_user), invites=Depends(get_invites)
):
context = {"request": request, "user": user, "invites": invites}
return templates.TemplateResponse("admin.html", context=context)
@app.get("/login")
async def login(request: Request):
return templates.TemplateResponse(
"login.html", context={"request": request}
)
@app.get("/login/keycloak") @app.get("/login/keycloak")
@ -50,14 +126,39 @@ async def login_keycloak(request: Request):
async def auth_keycloak(request: Request): async def auth_keycloak(request: Request):
try: try:
token = await oauth.keycloak.authorize_access_token(request) token = await oauth.keycloak.authorize_access_token(request)
user = await oauth.keycloak.parse_id_token(request, token) except Exception as exc:
request.session["user"] = dict(user) return HTMLResponse(f"<p>{exc} (<a href='{home}'>home</a>)</p>")
return RedirectResponse(request.url_for("home"))
except Exception as exception:
return HTMLResponse(f"<h1>{str(exception)}</h1>")
user = await oauth.keycloak.parse_id_token(request, token)
request.session["user"] = dict(user)
@app.route("/logout")
async def logout(request: Request):
request.session.pop("user", None)
return RedirectResponse(request.url_for("home")) return RedirectResponse(request.url_for("home"))
@app.get("/logout", dependencies=[Depends(logged_in)])
async def logout(request: Request):
try:
httpx.get(f"{BASE_URL}/logout")
except Exception as exc:
return HTMLResponse(f"<p>{exc} (<a href='{home}'>home</a>)</p>")
request.session.pop("user", None)
return RedirectResponse(request.url_for("login"))
@app.get("/invite/keycloak/create", dependencies=[Depends(logged_in)])
async def invite_keycloak_create(
request: Request, user=Depends(get_user), invites=Depends(get_invites)
):
invites.append({"link": str(uuid4()), "time": str(dt.now())})
app.state.redis.set(user["preferred_username"], json.dumps(invites))
return RedirectResponse(request.url_for("home"))
@app.get("/invite/keycloak/delete", dependencies=[Depends(logged_in)])
async def invite_keycloak_delete(
request: Request, user=Depends(get_user), invites=Depends(get_invites)
):
invite =request.query_params.get("invite")
# TODO

View File

@ -1,5 +1,5 @@
.DEFAULT: run .DEFAULT: run
.PHONY: run .PHONY: run redis
run: run:
@if [ ! -d ".venv" ]; then \ @if [ ! -d ".venv" ]; then \
@ -8,3 +8,6 @@ run:
.venv/bin/poetry install --dev; \ .venv/bin/poetry install --dev; \
fi fi
.venv/bin/poetry run uvicorn keycloak_collective_portal:app --reload .venv/bin/poetry run uvicorn keycloak_collective_portal:app --reload
redis:
@docker run -p 6379:6379 --name redis -d redis:6-alpine

96
poetry.lock generated
View File

@ -1,3 +1,15 @@
[[package]]
name = "aioredis"
version = "1.3.1"
description = "asyncio (PEP 3156) Redis support"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
async-timeout = "*"
hiredis = "*"
[[package]] [[package]]
name = "anyio" name = "anyio"
version = "3.1.0" version = "3.1.0"
@ -34,6 +46,14 @@ python-versions = ">=3.6"
[package.extras] [package.extras]
tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
[[package]]
name = "async-timeout"
version = "3.0.1"
description = "Timeout context manager for asyncio programs"
category = "main"
optional = false
python-versions = ">=3.5.3"
[[package]] [[package]]
name = "authlib" name = "authlib"
version = "0.15.4" version = "0.15.4"
@ -166,6 +186,14 @@ category = "main"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[[package]]
name = "hiredis"
version = "2.0.0"
description = "Python wrapper for hiredis"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]] [[package]]
name = "httpcore" name = "httpcore"
version = "0.13.4" version = "0.13.4"
@ -211,6 +239,17 @@ sniffio = "*"
brotli = ["brotlicffi (>=1.0.0,<2.0.0)"] brotli = ["brotlicffi (>=1.0.0,<2.0.0)"]
http2 = ["h2 (>=3.0.0,<4.0.0)"] http2 = ["h2 (>=3.0.0,<4.0.0)"]
[[package]]
name = "humanize"
version = "3.7.1"
description = "Python humanize utilities"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
tests = ["freezegun", "pytest", "pytest-cov"]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.2" version = "3.2"
@ -473,9 +512,13 @@ python-versions = ">=3.6.1"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "5c484b3f866449256a1928794c2787de8672804952c7887f059cb944beecdaf1" content-hash = "0ba44935dd5a68706610677a7e54fa3ebcd6f5a68edd05a1e1ecb8dbd9ca5947"
[metadata.files] [metadata.files]
aioredis = [
{file = "aioredis-1.3.1-py3-none-any.whl", hash = "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"},
{file = "aioredis-1.3.1.tar.gz", hash = "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a"},
]
anyio = [ anyio = [
{file = "anyio-3.1.0-py3-none-any.whl", hash = "sha256:5e335cef65fbd1a422bbfbb4722e8e9a9fadbd8c06d5afe9cd614d12023f6e5a"}, {file = "anyio-3.1.0-py3-none-any.whl", hash = "sha256:5e335cef65fbd1a422bbfbb4722e8e9a9fadbd8c06d5afe9cd614d12023f6e5a"},
{file = "anyio-3.1.0.tar.gz", hash = "sha256:43e20711a9d003d858d694c12356dc44ab82c03ccc5290313c3392fa349dad0e"}, {file = "anyio-3.1.0.tar.gz", hash = "sha256:43e20711a9d003d858d694c12356dc44ab82c03ccc5290313c3392fa349dad0e"},
@ -488,6 +531,10 @@ asgiref = [
{file = "asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee"}, {file = "asgiref-3.3.4-py3-none-any.whl", hash = "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee"},
{file = "asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"}, {file = "asgiref-3.3.4.tar.gz", hash = "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"},
] ]
async-timeout = [
{file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
{file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
]
authlib = [ authlib = [
{file = "Authlib-0.15.4-py2.py3-none-any.whl", hash = "sha256:d9fe5edb59801b16583faa86f88d798d99d952979b9616d5c735b9170b41ae2c"}, {file = "Authlib-0.15.4-py2.py3-none-any.whl", hash = "sha256:d9fe5edb59801b16583faa86f88d798d99d952979b9616d5c735b9170b41ae2c"},
{file = "Authlib-0.15.4.tar.gz", hash = "sha256:37df3a2554bc6fe0da3cc6848c44fac2ae40634a7f8fc72543947f4330b26464"}, {file = "Authlib-0.15.4.tar.gz", hash = "sha256:37df3a2554bc6fe0da3cc6848c44fac2ae40634a7f8fc72543947f4330b26464"},
@ -585,6 +632,49 @@ h11 = [
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
{file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
] ]
hiredis = [
{file = "hiredis-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05"},
{file = "hiredis-2.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a"},
{file = "hiredis-2.0.0-cp36-cp36m-win32.whl", hash = "sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63"},
{file = "hiredis-2.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6"},
{file = "hiredis-2.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e"},
{file = "hiredis-2.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79"},
{file = "hiredis-2.0.0-cp37-cp37m-win32.whl", hash = "sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc"},
{file = "hiredis-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"},
{file = "hiredis-2.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db"},
{file = "hiredis-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048"},
{file = "hiredis-2.0.0-cp38-cp38-win32.whl", hash = "sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426"},
{file = "hiredis-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581"},
{file = "hiredis-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0"},
{file = "hiredis-2.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e"},
{file = "hiredis-2.0.0-cp39-cp39-win32.whl", hash = "sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d"},
{file = "hiredis-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9"},
{file = "hiredis-2.0.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54"},
{file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27"},
{file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d"},
{file = "hiredis-2.0.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163"},
{file = "hiredis-2.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a"},
{file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87"},
{file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41"},
{file = "hiredis-2.0.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0"},
{file = "hiredis-2.0.0.tar.gz", hash = "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a"},
]
httpcore = [ httpcore = [
{file = "httpcore-0.13.4-py3-none-any.whl", hash = "sha256:38e09649bb3906c913a2917c4eb3e3b3e11c83d4edebad8b53b7d757abc49267"}, {file = "httpcore-0.13.4-py3-none-any.whl", hash = "sha256:38e09649bb3906c913a2917c4eb3e3b3e11c83d4edebad8b53b7d757abc49267"},
{file = "httpcore-0.13.4.tar.gz", hash = "sha256:9fa4c623bb9d2280c009c34658cc6315e4fd425a395145645bee205d827263e4"}, {file = "httpcore-0.13.4.tar.gz", hash = "sha256:9fa4c623bb9d2280c009c34658cc6315e4fd425a395145645bee205d827263e4"},
@ -610,6 +700,10 @@ httpx = [
{file = "httpx-0.18.1-py3-none-any.whl", hash = "sha256:ad2e3db847be736edc4b272c4d5788790a7e5789ef132fc6b5fef8aeb9e9f6e0"}, {file = "httpx-0.18.1-py3-none-any.whl", hash = "sha256:ad2e3db847be736edc4b272c4d5788790a7e5789ef132fc6b5fef8aeb9e9f6e0"},
{file = "httpx-0.18.1.tar.gz", hash = "sha256:0a2651dd2b9d7662c70d12ada5c290abcf57373b9633515fe4baa9f62566086f"}, {file = "httpx-0.18.1.tar.gz", hash = "sha256:0a2651dd2b9d7662c70d12ada5c290abcf57373b9633515fe4baa9f62566086f"},
] ]
humanize = [
{file = "humanize-3.7.1-py3-none-any.whl", hash = "sha256:a0dca9eb010dd1fab61819acaea54be344a4c22c77261f72ac4dbee183dd9a59"},
{file = "humanize-3.7.1.tar.gz", hash = "sha256:b8e7878f3063174b212bb82b9e5bee3b24bc47931e44df0bd34bcb1d8e0acf2f"},
]
idna = [ idna = [
{file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
{file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},

View File

@ -13,6 +13,8 @@ Jinja2 = "^3.0.1"
itsdangerous = "^2.0.1" itsdangerous = "^2.0.1"
Authlib = "^0.15.4" Authlib = "^0.15.4"
httpx = "^0.18.1" httpx = "^0.18.1"
aioredis = "^1.3.1"
humanize = "^3.7.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
black = "^21.6b0" black = "^21.6b0"

28
templates/admin.html Normal file
View File

@ -0,0 +1,28 @@
<html>
<head>
<title>Home</title>
</head>
<body>
<p>
Hello, {{ user.preferred_username }}
<small>(<a href="{{ url_for('logout') }}">logout</a>)</small>
</p>
<table>
<tr>
<th>Link</th>
<th>Validity </th>
<th>Operations</th>
</tr>
{% for invite in invites %}
<tr>
<td> <a class="invite" href="{{ url_for('home') }}{{ invite.link }}">{{ url_for('home') }}{{ invite.link }}</a> </td>
<td> {{ invite.human_time }} </td>
<td> <a href="{{ url_for('invite_keycloak_delete') }}?invite={{ invite.link }}">delete</a> </td>
</tr>
{% endfor %}
</table>
<p>
<a href="{{ url_for('invite_keycloak_create') }}">Generate an invite link</a>
</p>
</body>
</html>

View File

@ -1,8 +0,0 @@
<html>
<head>
<title>Home</title>
</head>
<body>
<p>Hello, {{ user.preferred_username }}</p>
</body>
</html>

View File

@ -3,6 +3,8 @@
<title>Login</title> <title>Login</title>
</head> </head>
<body> <body>
<p>Please login</p> <p>
<a href="{{ url_for('login_keycloak') }}">Login</a>
</p>
</body> </body>
</html> </html>