decentral1se 4 months ago
parent
commit
d22abd2bc5
Signed by: decentral1se GPG Key ID: 92DAD76BD9567B8A
  1. 4
      Dockerfile
  2. 9
      README.md
  3. 239
      keycloak_collective_portal.py
  4. 0
      keycloak_collective_portal/__init__.py
  5. 31
      keycloak_collective_portal/config.py
  6. 42
      keycloak_collective_portal/dependencies.py
  7. 7
      keycloak_collective_portal/exceptions.py
  8. 21
      keycloak_collective_portal/keycloak.py
  9. 71
      keycloak_collective_portal/main.py
  10. 25
      keycloak_collective_portal/oidc.py
  11. 39
      keycloak_collective_portal/redis.py
  12. 10
      keycloak_collective_portal/routes/health.py
  13. 49
      keycloak_collective_portal/routes/invite.py
  14. 53
      keycloak_collective_portal/routes/oidc.py
  15. 96
      keycloak_collective_portal/routes/register.py
  16. 21
      keycloak_collective_portal/routes/root.py
  17. 3
      keycloak_collective_portal/static/default.css
  18. 3
      keycloak_collective_portal/static/lumbung.css
  19. 28
      keycloak_collective_portal/templates/admin.html
  20. 18
      keycloak_collective_portal/templates/base.html
  21. 5
      keycloak_collective_portal/templates/invalid.html
  22. 6
      keycloak_collective_portal/templates/login.html
  23. 26
      keycloak_collective_portal/templates/register.html
  24. 10
      keycloak_collective_portal/templates/submit.html
  25. 2
      makefile
  26. 14
      poetry.lock
  27. 1
      pyproject.toml
  28. 1
      styles.css
  29. 28
      templates/admin.html
  30. 10
      templates/login.html
  31. 44
      templates/register.html

4
Dockerfile

@ -15,5 +15,7 @@ RUN pip install --no-cache-dir --disable-pip-version-check --no-index --no-deps
COPY . /srv/project
WORKDIR /srv/project
CMD ["uvicorn", "--forwarded-allow-ips='*'", "--proxy-headers", "--host", "0.0.0.0", "keycloak_collective_portal:app"]
RUN apt update && apt install -yq curl
CMD ["uvicorn", "--forwarded-allow-ips='*'", "--proxy-headers", "--host", "0.0.0.0", "keycloak_collective_portal.main:app"]
EXPOSE 8000

9
README.md

@ -40,6 +40,7 @@ your technology stack.
- **Service Account Roles tab**:
- **Client roles**: Under `realm-management` add `manage-users` and `view-users`
- Deploy using [`coop-cloud/keycloak-colective-portal`](https://git.autonomic.zone/coop-cloud/keycloak-collective-portal)
- See the example [.envrc](./.envrc) for the configuration available, more documentation will follow soon.
### From a collective member perspective
@ -49,9 +50,11 @@ your technology stack.
## Hacking
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.
It's a [FastAPI](https://fastapi.tiangolo.com/) application (if you know
[Flask](https://flask.palletsprojects.com/en/2.0.x/) /
[Sanic](https://sanic.readthedocs.io/en/stable/) then it is more or less the
same thing). 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

239
keycloak_collective_portal.py

@ -1,239 +0,0 @@
"""Community Keycloak SSO user management."""
import json
from datetime import datetime as dt
from datetime import timedelta
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 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
APP_SECRET_KEY = environ.get("APP_SECRET_KEY")
KEYCLOAK_CLIENT_ID = environ.get("KEYCLOAK_CLIENT_ID")
KEYCLOAK_CLIENT_SECRET = environ.get("KEYCLOAK_CLIENT_SECRET")
KEYCLOAK_DOMAIN = environ.get("KEYCLOAK_DOMAIN")
KEYCLOAK_REALM = environ.get("KEYCLOAK_REALM")
BASE_URL = f"https://{KEYCLOAK_DOMAIN}/auth/realms/{KEYCLOAK_REALM}/protocol/openid-connect" # noqa
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)
templates = Jinja2Templates(directory="templates")
class RequiresLoginException(Exception):
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")
if not user:
raise RequiresLoginException
return user
async def get_user(request: Request):
return request.session.get("user")
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):
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"
)
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():
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")
async def login_keycloak(request: Request):
redirect_uri = request.url_for("auth_keycloak")
return await app.state.oauth.keycloak.authorize_redirect(
request, redirect_uri
)
@app.get("/auth/keycloak")
async def auth_keycloak(request: Request):
try:
token = await app.state.oauth.keycloak.authorize_access_token(request)
except Exception as exc:
return HTMLResponse(f"<p>{exc} (<a href='{home}'>home</a>)</p>")
user = await app.state.oauth.keycloak.parse_id_token(request, token)
request.session["user"] = dict(user)
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())})
await app.state.redis.set(user["preferred_username"], json.dumps(invites))
print(invites, 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_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)

0
keycloak_collective_portal/__init__.py

31
keycloak_collective_portal/config.py

@ -0,0 +1,31 @@
"""Application configuraiton."""
from datetime import timedelta
from os import environ
from pathlib import Path
# Application secret key, used for the SessionMiddleware
APP_SECRET_KEY = environ.get("APP_SECRET_KEY")
# Keycloak client details
KEYCLOAK_CLIENT_ID = environ.get("KEYCLOAK_CLIENT_ID")
KEYCLOAK_CLIENT_SECRET = environ.get("KEYCLOAK_CLIENT_SECRET")
KEYCLOAK_DOMAIN = environ.get("KEYCLOAK_DOMAIN")
KEYCLOAK_REALM = environ.get("KEYCLOAK_REALM")
KEYCLOAK_SCOPES = environ.get("KEYCLOAK_SCOPES", "openid profile email")
KEYCLOAK_BASE_URL = f"https://{KEYCLOAK_DOMAIN}/auth/realms/{KEYCLOAK_REALM}/protocol/openid-connect" # noqa
# Redis connection details, our main storage
REDIS_DB = environ.get("REDIS_DB")
REDIS_HOST = environ.get("REDIS_HOST")
REDIS_PORT = environ.get("REDIS_PORT")
# How many days do we want the invites to be valid for?
INVITE_TIME_LIMIT = int(environ.get("INVITE_TIME_LIMIT", 30))
# Static and template configuration
STATIC_DIR = Path(".").absolute() / "keycloak_collective_portal" / "static"
TEMPLATE_DIR = Path(".").absolute() / "keycloak_collective_portal" / "templates"
# Theme selection
APP_THEME = environ.get("APP_THEME", "default")

42
keycloak_collective_portal/dependencies.py

@ -0,0 +1,42 @@
"""Route dependencies."""
from datetime import datetime as dt
from datetime import timedelta
from fastapi import Depends, Request
from humanize import naturaldelta
async def logged_in(request: Request):
"""Ensure the user is logged in."""
from keycloak_collective_portal.exceptions import RequiresLoginException
user = request.session.get("user")
if not user:
raise RequiresLoginException
return user
async def get_user(request: Request):
"""Retrieve the user object."""
return request.session.get("user")
async def get_invites(request: Request, user=Depends(get_user)):
"""Retrieve all invites from storage."""
from keycloak_collective_portal.config import INVITE_TIME_LIMIT
all_invites = {}
for username in await request.app.state.redis.keys("*"):
invites = await request.app.state.redis.get(username)
for invite in invites:
invite["validity"] = naturaldelta(
dt.fromisoformat(invite["time"])
+ timedelta(days=INVITE_TIME_LIMIT)
)
all_invites[username] = invites
return all_invites

7
keycloak_collective_portal/exceptions.py

@ -0,0 +1,7 @@
"""Exceptions."""
class RequiresLoginException(Exception):
"""An exception thrown if the user is not logged in."""
pass

21
keycloak_collective_portal/keycloak.py

@ -0,0 +1,21 @@
"""Keycloak logic."""
from keycloak import KeycloakAdmin
def init_keycloak():
"""Initialise Keycloak client."""
from keycloak_collective_portal.config import (
KEYCLOAK_CLIENT_SECRET,
KEYCLOAK_DOMAIN,
KEYCLOAK_REALM,
)
client = KeycloakAdmin(
server_url=f"https://{KEYCLOAK_DOMAIN}/auth/",
realm_name=KEYCLOAK_REALM,
client_secret_key=KEYCLOAK_CLIENT_SECRET,
verify=True,
)
return client

71
keycloak_collective_portal/main.py

@ -0,0 +1,71 @@
"""App entrypoint."""
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.middleware.sessions import SessionMiddleware
from keycloak_collective_portal.config import (
APP_SECRET_KEY,
APP_THEME,
REDIS_DB,
REDIS_HOST,
REDIS_PORT,
STATIC_DIR,
TEMPLATE_DIR,
)
from keycloak_collective_portal.exceptions import RequiresLoginException
from keycloak_collective_portal.keycloak import init_keycloak
from keycloak_collective_portal.oidc import init_oidc
from keycloak_collective_portal.redis import Redis
from keycloak_collective_portal.routes import (
health,
invite,
oidc,
register,
root,
)
app = FastAPI(docs_url=None, redoc_url=None)
@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>")
@app.on_event("startup")
async def starup_event():
redis = Redis()
app.state.redis = await redis.create_pool(
f"redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}?encoding=utf-8"
)
@app.on_event("shutdown")
async def shutdown_event():
app.state.redis.close()
app.add_middleware(SessionMiddleware, secret_key=APP_SECRET_KEY)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
app.state.oidc = init_oidc()
app.state.keycloak = init_keycloak()
app.state.templates = Jinja2Templates(directory=TEMPLATE_DIR)
app.state.theme = APP_THEME
app.include_router(invite.router)
app.include_router(oidc.router)
app.include_router(register.router)
app.include_router(root.router)
app.include_router(health.router)

25
keycloak_collective_portal/oidc.py

@ -0,0 +1,25 @@
"""OpenID Connect logic."""
from authlib.integrations.starlette_client import OAuth, OAuthError
def init_oidc():
"""Initialise OIDC client."""
from keycloak_collective_portal.config import (
KEYCLOAK_BASE_URL,
KEYCLOAK_CLIENT_ID,
KEYCLOAK_CLIENT_SECRET,
KEYCLOAK_SCOPES,
)
oidc = OAuth()
oidc.register(
name="keycloak",
client_kwargs={"scope": KEYCLOAK_SCOPES},
client_id=KEYCLOAK_CLIENT_ID,
client_secret=KEYCLOAK_CLIENT_SECRET,
authorize_url=f"{KEYCLOAK_BASE_URL}/auth",
access_token_url=f"{KEYCLOAK_BASE_URL}/token",
jwks_uri=f"{KEYCLOAK_BASE_URL}/certs",
)
return oidc

39
keycloak_collective_portal/redis.py

@ -0,0 +1,39 @@
"""Redis cache."""
import json
from aioredis import create_redis_pool
class Redis:
"""Redis cache."""
def __init__(self):
"""Initialise the object."""
self._redis = None
async def create_pool(self, conn):
"""Initialise pool."""
self._redis = await create_redis_pool(conn)
return self
async def keys(self, pattern):
"""Retrieve keys that match a pattern."""
return await self._redis.keys(pattern)
async def set(self, key, value, dumps=True):
"""Set a key."""
if dumps:
return await self._redis.set(key, json.dumps(value))
return await self._redis.set(key, value)
async def get(self, key, loads=True):
"""Get a specific key."""
if loads:
return json.loads(await self._redis.get(key))
return await self._redis.get(key)
async def close(self):
"""Close the connection."""
self.redis_cache.close()
await self.redis_cache.wait_closed()

10
keycloak_collective_portal/routes/health.py

@ -0,0 +1,10 @@
"""Healthcheck routes."""
from fastapi import APIRouter, Request
router = APIRouter()
@router.get("/healthz")
async def healthz(request: Request):
return {"detail": "ALL ENGINES FIRING"}

49
keycloak_collective_portal/routes/invite.py

@ -0,0 +1,49 @@
"""Routes for invite logic."""
from datetime import datetime as dt
from uuid import uuid4
from fastapi import APIRouter, Depends, Request
from fastapi.responses import RedirectResponse
from keycloak_collective_portal.dependencies import (
get_invites,
get_user,
logged_in,
)
router = APIRouter()
@router.get("/invite/keycloak/create", dependencies=[Depends(logged_in)])
async def invite_keycloak_create(
request: Request, user=Depends(get_user), invites=Depends(get_invites)
):
username = user["preferred_username"]
new_invite = {"link": str(uuid4()), "time": str(dt.now())}
invites = await request.app.state.redis.get(username)
if invites:
invites.append(new_invite)
else:
invites = [new_invite]
await request.app.state.redis.set(username, invites)
return RedirectResponse(request.url_for("home"))
@router.get("/invite/keycloak/delete", dependencies=[Depends(logged_in)])
async def invite_keycloak_delete(
request: Request, user=Depends(get_user), invites=Depends(get_invites)
):
username = user["preferred_username"]
invite_to_delete = request.query_params.get("invite")
invites = await request.app.state.redis.get(username)
purged = [i for i in invites if i["link"] != invite_to_delete]
await request.app.state.redis.set(user["preferred_username"], purged)
return RedirectResponse(request.url_for("home"))

53
keycloak_collective_portal/routes/oidc.py

@ -0,0 +1,53 @@
"""OpenID Connect routes."""
import httpx
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from keycloak_collective_portal.dependencies import logged_in
router = APIRouter()
@router.get("/login")
async def login(request: Request):
return request.app.state.templates.TemplateResponse(
"login.html", context={"request": request}
)
@router.get("/login/keycloak")
async def login_keycloak(request: Request):
redirect_uri = request.url_for("auth_keycloak")
return await request.app.state.oidc.keycloak.authorize_redirect(
request, redirect_uri
)
@router.get("/auth/keycloak")
async def auth_keycloak(request: Request):
try:
token = await request.app.state.oidc.keycloak.authorize_access_token(
request
)
except Exception as exc:
return HTMLResponse(f"<p>{exc} (<a href='/'>home</a>)</p>")
user = await request.app.state.oidc.keycloak.parse_id_token(request, token)
request.session["user"] = dict(user)
return RedirectResponse(request.url_for("home"))
@router.get("/logout", dependencies=[Depends(logged_in)])
async def logout(request: Request):
from keycloak_collective_portal.config import KEYCLOAK_BASE_URL
try:
httpx.get(f"{KEYCLOAK_BASE_URL}/logout")
except Exception as exc:
return HTMLResponse(f"<p>{exc} (<a href='/'>home</a>)</p>")
request.session.pop("user", None)
return RedirectResponse(request.url_for("login"))

96
keycloak_collective_portal/routes/register.py

@ -0,0 +1,96 @@
"""Registration routes."""
import json
from datetime import datetime as dt
from datetime import timedelta
from fastapi import APIRouter, Depends, Form, Request
from keycloak_collective_portal.dependencies import get_invites
router = APIRouter()
@router.get("/register/{invite}")
async def register_invite(
request: Request, invite: str, invites=Depends(get_invites)
):
from keycloak_collective_portal.config import INVITE_TIME_LIMIT
matching, username, matching_invite = False, None, None
for username in invites:
for _invite in invites[username]:
if invite == _invite["link"]:
matching = True
username = username
matching_invite = _invite
if not matching:
message = "This invite does not exist, sorry."
context = {"request": request, "message": message}
return request.app.state.templates.TemplateResponse(
"invalid.html", context=context
)
expired = (
dt.fromisoformat(matching_invite["time"])
+ timedelta(days=INVITE_TIME_LIMIT)
).day > dt.now().day
if expired:
message = "This invite has expired, sorry."
context = {"request": request, "message": message}
return request.app.state.templates.TemplateResponse(
"invalid.html", context=context
)
context = {"request": request, "username": username}
return request.app.state.templates.TemplateResponse(
"register.html", context=context
)
@router.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(...),
):
payload = {
"email": email,
"username": username,
"enabled": True,
"firstName": first_name,
"lastName": last_name,
"credentials": [
{
"value": password,
"type": "password",
}
],
"realmRoles": [
"user_default",
],
}
try:
user_id = request.app.state.keycloak.create_user(
payload, exist_ok=False
)
request.app.state.keycloak.send_verify_email(user_id=user_id)
except Exception as exception:
message = json.loads(exception.error_message).get(
"errorMessage", "Unknown reason!"
)
context = {"request": request, "exception": message}
return request.app.state.templates.TemplateResponse(
"submit.html", context=context
)
context = {"request": request, "email": email}
return request.app.state.templates.TemplateResponse(
"submit.html", context=context
)

21
keycloak_collective_portal/routes/root.py

@ -0,0 +1,21 @@
"""Home routes."""
from fastapi import APIRouter, Depends, Request
from keycloak_collective_portal.dependencies import (
get_invites,
get_user,
logged_in,
)
router = APIRouter()
@router.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 request.app.state.templates.TemplateResponse(
"admin.html", context=context
)

3
keycloak_collective_portal/static/default.css

@ -0,0 +1,3 @@
input {
display: block;
}

3
keycloak_collective_portal/static/lumbung.css

@ -0,0 +1,3 @@
input {
display: block;
}

28
keycloak_collective_portal/templates/admin.html

@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block content %}
<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[user.preferred_username] %}
<tr>
<td>
<a class="invite" href="{{ url_for('register_invite', invite=invite.link) }}">
{{ url_for('register_invite', invite=invite.link) }}
</a>
</td>
<td> {{ invite.validity }} </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>
{% endblock %}

18
keycloak_collective_portal/templates/base.html

@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
{% block head %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% if request.app.state.theme == "default" %}
<link href="{{ url_for('static', path='/default.css') }}" rel="stylesheet">
{% elif request.app.state.theme == "lumbung" %}
<link href="{{ url_for('static', path='/lumbung.css') }}" rel="stylesheet">
{% endif %}
<title>{% block title %}{% endblock %}</title>
{% endblock %}
</head>
<body>
<div id="content">{% block content %}{% endblock %}</div>
</body>
</html>

5
keycloak_collective_portal/templates/invalid.html

@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block content %}
<p>Woops, something went wrong: {{ message }}</p>
<p>Please contact your system adminstrator if this is unexpected.</p>
{% endblock %}

6
keycloak_collective_portal/templates/login.html

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block content %}
<p>
<a href="{{ url_for('login_keycloak') }}">Login</a>
</p>
{% endblock %}

26
keycloak_collective_portal/templates/register.html

@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block content %}
<p>
You've been invited by {{ username }} to register an account!
</p>
<form method="post" action="{{ url_for('form_keycloak_register') }}">
<label for="first_name">First name:</label>
<input type="text" name="first_name" />
<label for="last_name">Last name:</label>
<input type="text" name="last_name" />
<label for="username">Username:</label>
<input type="text" name="username" />
<label for="email">Email:</label>
<input type="text" name="email" />
<label for="password">Password:</label>
<input type="password" name="password" />
<input type="submit" />
</form>
{% endblock %}

10
keycloak_collective_portal/templates/submit.html

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
{% if email %}
<p>Thank you! You will receive a welcome mail to {{ email }} shortly.</p>
<p>Don't forget to check your Spam folder, in case the email ends up there.</p>
{% elif exception %}
<p>Woops, something went wrong: {{ exception }}.</p>
<p>Please contact your system adminstrator if this is unexpected.</p>
{% endif %}
{% endblock %}

2
makefile

@ -7,7 +7,7 @@ run:
.venv/bin/pip install -U pip setuptools wheel poetry && \
.venv/bin/poetry install --dev; \
fi
.venv/bin/poetry run uvicorn keycloak_collective_portal:app --reload
.venv/bin/poetry run uvicorn keycloak_collective_portal.main:app --reload
redis:
@docker run -p 6379:6379 --name redis -d redis:6-alpine

14
poetry.lock

@ -1,3 +1,11 @@
[[package]]
name = "aiofiles"
version = "0.7.0"
description = "File support for asyncio."
category = "main"
optional = false
python-versions = ">=3.6,<4.0"
[[package]]
name = "aioredis"
version = "1.3.1"
@ -634,9 +642,13 @@ python-versions = ">=3.6.1"
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "6df09118b73a6b5e1a00cd4bc78d47d4b35faec4af719bd10f9f8a84bdd57ef0"
content-hash = "d8f978355587c9f76a7888c64b7d1409de886670a4b6a51cdfc0eedbd6ba3009"
[metadata.files]
aiofiles = [
{file = "aiofiles-0.7.0-py3-none-any.whl", hash = "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc"},
{file = "aiofiles-0.7.0.tar.gz", hash = "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4"},
]
aioredis = [
{file = "aioredis-1.3.1-py3-none-any.whl", hash = "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"},
{file = "aioredis-1.3.1.tar.gz", hash = "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a"},

1
pyproject.toml

@ -17,6 +17,7 @@ aioredis = "^1.3.1"
humanize = "^3.7.1"
python-multipart = "^0.0.5"
python-keycloak = "^0.25.0"
aiofiles = "^0.7.0"
[tool.poetry.dev-dependencies]
black = "^21.6b0"

1
styles.css

@ -1 +0,0 @@
// TODO

28
templates/admin.html

@ -1,28 +0,0 @@
<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('register_invite', invite=invite.link) }}">{{ url_for('register_invite', invite=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>

10
templates/login.html

@ -1,10 +0,0 @@
<html>
<head>
<title>Login</title>
</head>
<body>
<p>
<a href="{{ url_for('login_keycloak') }}">Login</a>
</p>
</body>
</html>

44
templates/register.html

@ -1,44 +0,0 @@
<html>
<head>
<title>Register</title>
<style>
input {
display: block;
}
</style>
</head>
<body>
<p>
You've been invited by {{ username }} to register an account!
</p>
<form method="post" action="{{ url_for('form_keycloak_register') }}">
<label for="first_name">First name:</label>
<input type="text" name="first_name" />
<label for="last_name">Last name:</label>
<input type="text" name="last_name" />
<label for="username">Username:</label>
<input type="text" name="username" />
<label for="email">Email:</label>
<input type="text" name="email" />
<label for="password">Password:</label>
<input type="password" name="password" />
<input type="submit" />
</form>
{% if success %}
<p>
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.
</p>
{% elif failure %}
<p>Something went wrong, oops! Please contact the system administrator.</p>
{% endif %}
</body>
</html>
Loading…
Cancel
Save