diff --git a/README.md b/README.md index 1257a44..6442deb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # keycloak-collective-portal +> **WARNING**: this software is in a pre-alpha quality state and is an initial +> prototype. It is being developed within the context of +> [lumbung.space](https://lumbung.space/) and maybe have hard-coded values and +> configuration specifically for that environment. If the idea of this software +> sounds interesting to you, please let us know on the issue tracker! + [![Build Status](https://drone.autonomic.zone/api/badges/autonomic-cooperative/keycloak-collective-portal/status.svg?ref=refs/heads/main)](https://drone.autonomic.zone/autonomic-cooperative/keycloak-collective-portal) > Community Keycloak SSO user management @@ -42,9 +48,12 @@ your technology stack. ## Hacking -It's a [FastAPI](https://fastapi.tiangolo.com/) application. +It's a [FastAPI](https://fastapi.tiangolo.com/) application. Currently being +developed with Python 3.9. Once we move out of the prototype stage, more +version compatability will be offered. ``` +$ docker run -p 6379:6379 -d redis:6-alpine $ set -a && source .envrc && set +a $ make ``` diff --git a/keycloak_collective_portal.py b/keycloak_collective_portal.py index 9aff685..08944f1 100644 --- a/keycloak_collective_portal.py +++ b/keycloak_collective_portal.py @@ -9,10 +9,11 @@ from uuid import uuid4 import httpx from aioredis import create_redis_pool from authlib.integrations.starlette_client import OAuth, OAuthError -from fastapi import Depends, FastAPI, HTTPException, Request +from fastapi import Depends, FastAPI, Form, HTTPException, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates from humanize import naturaldelta +from keycloak import KeycloakAdmin from starlette.exceptions import HTTPException from starlette.middleware.sessions import SessionMiddleware @@ -35,17 +36,6 @@ app = FastAPI(docs_url=None, redoc_url=None) app.add_middleware(SessionMiddleware, secret_key=APP_SECRET_KEY) templates = Jinja2Templates(directory="templates") -oauth = OAuth() -oauth.register( - name="keycloak", - client_kwargs={"scope": "openid profile email"}, - client_id=KEYCLOAK_CLIENT_ID, - client_secret=KEYCLOAK_CLIENT_SECRET, - authorize_url=f"{BASE_URL}/auth", - access_token_url=f"{BASE_URL}/token", - jwks_uri=f"{BASE_URL}/certs", -) - class RequiresLoginException(Exception): pass @@ -74,8 +64,18 @@ async def get_user(request: Request): async def get_invites(request: Request, user=Depends(get_user)): + if not user: + idx, invites = b"0", {} + while idx: + idx, username = await app.state.redis.scan(idx) + invites[username[0]] = json.loads( + await app.state.redis.get(username[0]) + ) + return invites + username = user["preferred_username"] invites = await app.state.redis.get(username) + if invites: humanised = [] for invite in json.loads(invites): @@ -85,6 +85,7 @@ async def get_invites(request: Request, user=Depends(get_user)): ) humanised.append(invite) return humanised + return [] @@ -94,6 +95,25 @@ async def starup_event(): f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}?encoding=utf-8" ) + oauth = OAuth() + oauth.register( + name="keycloak", + client_kwargs={"scope": "openid profile email"}, + client_id=KEYCLOAK_CLIENT_ID, + client_secret=KEYCLOAK_CLIENT_SECRET, + authorize_url=f"{BASE_URL}/auth", + access_token_url=f"{BASE_URL}/token", + jwks_uri=f"{BASE_URL}/certs", + ) + app.state.oauth = oauth + + app.state.keycloak = KeycloakAdmin( + server_url=f"https://{KEYCLOAK_DOMAIN}/auth/", + realm_name=KEYCLOAK_REALM, + client_secret_key=KEYCLOAK_CLIENT_SECRET, + verify=True, + ) + @app.on_event("shutdown") async def shutdown_event(): @@ -119,17 +139,19 @@ async def login(request: Request): @app.get("/login/keycloak") async def login_keycloak(request: Request): redirect_uri = request.url_for("auth_keycloak") - return await oauth.keycloak.authorize_redirect(request, redirect_uri) + return await app.state.oauth.keycloak.authorize_redirect( + request, redirect_uri + ) @app.get("/auth/keycloak") async def auth_keycloak(request: Request): try: - token = await oauth.keycloak.authorize_access_token(request) + token = await app.state.oauth.keycloak.authorize_access_token(request) except Exception as exc: return HTMLResponse(f"

{exc} (home)

") - user = await oauth.keycloak.parse_id_token(request, token) + user = await app.state.oauth.keycloak.parse_id_token(request, token) request.session["user"] = dict(user) return RedirectResponse(request.url_for("home")) @@ -152,7 +174,8 @@ 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)) + await app.state.redis.set(user["preferred_username"], json.dumps(invites)) + print(invites, json.dumps(invites)) return RedirectResponse(request.url_for("home")) @@ -160,5 +183,57 @@ async def invite_keycloak_create( async def invite_keycloak_delete( request: Request, user=Depends(get_user), invites=Depends(get_invites) ): - invite =request.query_params.get("invite") - # TODO + invite_to_delete = request.query_params.get("invite") + purged = [i for i in invites if i["link"] != invite_to_delete] + await app.state.redis.set(user["preferred_username"], json.dumps(purged)) + return RedirectResponse(request.url_for("home")) + + +@app.get("/register/{invite}") +async def register_invite( + request: Request, invite: str, invites=Depends(get_invites) +): + matching, username = False, None + for username in invites: + if invite in [x["link"] for x in invites[username]]: + matching = True + username = username + + if not matching: + return RedirectResponse(request.url_for("login")) + + context = {"request": request, "username": username} + return templates.TemplateResponse("register.html", context=context) + + +@app.post("/form/keycloak/register") +def form_keycloak_register( + request: Request, + first_name: str = Form(...), + last_name: str = Form(...), + username: str = Form(...), + email: str = Form(...), + password: str = Form(...), +): + user_id = app.state.keycloak.create_user( + { + "email": email, + "username": username, + "enabled": True, + "firstName": first_name, + "lastName": last_name, + "credentials": [ + { + "value": password, + "type": "password", + } + ], + "realmRoles": [ + "user_default", + ], + } + ) + app.state.keycloak.send_verify_email(user_id=user_id) + + context = {"request": request, "success": True} + return templates.TemplateResponse("register.html", context=context) diff --git a/poetry.lock b/poetry.lock index 85b655c..1dacbe6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -109,6 +109,14 @@ python-versions = "*" [package.dependencies] pycparser = "*" +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "click" version = "8.0.1" @@ -147,6 +155,21 @@ sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +[[package]] +name = "ecdsa" +version = "0.17.0" +description = "ECDSA cryptographic signature library (pure python)" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +gmpy = ["gmpy"] +gmpy2 = ["gmpy2"] + [[package]] name = "fastapi" version = "0.65.2" @@ -252,11 +275,11 @@ tests = ["freezegun", "pytest", "pytest-cov"] [[package]] name = "idna" -version = "3.2" +version = "2.10" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "isort" @@ -342,6 +365,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pyasn1" +version = "0.4.8" +description = "ASN.1 types and codecs" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "pycodestyle" version = "2.7.0" @@ -392,6 +423,47 @@ python-versions = "*" [package.extras] cli = ["click (>=5.0)"] +[[package]] +name = "python-jose" +version = "3.3.0" +description = "JOSE implementation in Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +ecdsa = "!=0.15" +pyasn1 = "*" +rsa = "*" + +[package.extras] +cryptography = ["cryptography (>=3.4.0)"] +pycrypto = ["pycrypto (>=2.6.0,<2.7.0)", "pyasn1"] +pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)", "pyasn1"] + +[[package]] +name = "python-keycloak" +version = "0.25.0" +description = "python-keycloak is a Python package providing access to the Keycloak API." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +python-jose = ">=1.4.0" +requests = ">=2.20.0" + +[[package]] +name = "python-multipart" +version = "0.0.5" +description = "A streaming multipart parser for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.4.0" + [[package]] name = "pyyaml" version = "5.4.1" @@ -408,6 +480,24 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + [[package]] name = "rfc3986" version = "1.5.0" @@ -422,6 +512,25 @@ idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} [package.extras] idna2008 = ["idna"] +[[package]] +name = "rsa" +version = "4.7.2" +description = "Pure-Python RSA implementation" +category = "main" +optional = false +python-versions = ">=3.5, <4" + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "sniffio" version = "1.2.0" @@ -457,6 +566,19 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "urllib3" +version = "1.26.5" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + [[package]] name = "uvicorn" version = "0.14.0" @@ -512,7 +634,7 @@ python-versions = ">=3.6.1" [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "0ba44935dd5a68706610677a7e54fa3ebcd6f5a68edd05a1e1ecb8dbd9ca5947" +content-hash = "6df09118b73a6b5e1a00cd4bc78d47d4b35faec4af719bd10f9f8a84bdd57ef0" [metadata.files] aioredis = [ @@ -598,6 +720,10 @@ cffi = [ {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"}, {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"}, ] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] click = [ {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, @@ -620,6 +746,10 @@ cryptography = [ {file = "cryptography-3.4.7-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"}, {file = "cryptography-3.4.7.tar.gz", hash = "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713"}, ] +ecdsa = [ + {file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"}, + {file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"}, +] fastapi = [ {file = "fastapi-0.65.2-py3-none-any.whl", hash = "sha256:39569a18914075b2f1aaa03bcb9dc96a38e0e5dabaf3972e088c9077dfffa379"}, {file = "fastapi-0.65.2.tar.gz", hash = "sha256:8359e55d8412a5571c0736013d90af235d6949ec4ce978e9b63500c8f4b6f714"}, @@ -705,8 +835,8 @@ humanize = [ {file = "humanize-3.7.1.tar.gz", hash = "sha256:b8e7878f3063174b212bb82b9e5bee3b24bc47931e44df0bd34bcb1d8e0acf2f"}, ] idna = [ - {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, - {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] isort = [ {file = "isort-5.8.0-py3-none-any.whl", hash = "sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"}, @@ -793,6 +923,21 @@ pathspec = [ {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, ] +pyasn1 = [ + {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, + {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, + {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, + {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, + {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, + {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, + {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, + {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, + {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, + {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, +] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, @@ -833,6 +978,16 @@ python-dotenv = [ {file = "python-dotenv-0.17.1.tar.gz", hash = "sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"}, {file = "python_dotenv-0.17.1-py2.py3-none-any.whl", hash = "sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544"}, ] +python-jose = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] +python-keycloak = [ + {file = "python-keycloak-0.25.0.tar.gz", hash = "sha256:d02a7a4ed609583587482eacfdce409a00b633dff04ccf1cb3d478e1f0c50529"}, +] +python-multipart = [ + {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, +] pyyaml = [ {file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"}, {file = "PyYAML-5.4.1-cp27-cp27m-win32.whl", hash = "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393"}, @@ -907,10 +1062,22 @@ regex = [ {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"}, {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"}, ] +requests = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] +rsa = [ + {file = "rsa-4.7.2-py3-none-any.whl", hash = "sha256:78f9a9bf4e7be0c5ded4583326e7461e3a3c5aae24073648b4bdfa797d78c9d2"}, + {file = "rsa-4.7.2.tar.gz", hash = "sha256:9d689e6ca1b3038bc82bf8d23e944b6b6037bc02301a574935b2dd946e0353b9"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] sniffio = [ {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, @@ -928,6 +1095,10 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] +urllib3 = [ + {file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"}, + {file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"}, +] uvicorn = [ {file = "uvicorn-0.14.0-py3-none-any.whl", hash = "sha256:2a76bb359171a504b3d1c853409af3adbfa5cef374a4a59e5881945a97a93eae"}, {file = "uvicorn-0.14.0.tar.gz", hash = "sha256:45ad7dfaaa7d55cab4cd1e85e03f27e9d60bc067ddc59db52a2b0aeca8870292"}, diff --git a/pyproject.toml b/pyproject.toml index c8156bd..ea67945 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,8 @@ Authlib = "^0.15.4" httpx = "^0.18.1" aioredis = "^1.3.1" humanize = "^3.7.1" +python-multipart = "^0.0.5" +python-keycloak = "^0.25.0" [tool.poetry.dev-dependencies] black = "^21.6b0" diff --git a/templates/admin.html b/templates/admin.html index 9761ac6..a6e2e83 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -10,12 +10,12 @@ - + {% for invite in invites %} - + diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..11c7201 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,44 @@ + + + Register + + + +

+ You've been invited by {{ username }} to register an account! +

+ + + + + + + + + + + + + + + + + + + + + {% if success %} +

+ An email verification mail has been sent to {{ email }}. Please check your + mail shortly to verify account and set your account password. Please also + check your spam inbox in case it ends up in there. +

+ {% elif failure %} +

Something went wrong, oops! Please contact the system administrator.

+ {% endif %} + +
LinkValidity Validity Operations
{{ url_for('home') }}{{ invite.link }} {{ url_for('register_invite', invite=invite.link) }} {{ invite.human_time }} delete