From 0ca0472eff22cfa666291828d6a221a3a56a959d Mon Sep 17 00:00:00 2001 From: Rens Date: Thu, 9 Apr 2026 09:22:07 +0200 Subject: [PATCH 1/5] only magic link --- examples/magickey_auth/main.py | 29 ++++++++ examples/magickey_auth/requirements.txt | 2 + nbs/01_auth.ipynb | 93 ++++++++++++++++++++++--- plash_cli/_modidx.py | 3 + plash_cli/auth.py | 39 ++++++++++- 5 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 examples/magickey_auth/main.py create mode 100644 examples/magickey_auth/requirements.txt diff --git a/examples/magickey_auth/main.py b/examples/magickey_auth/main.py new file mode 100644 index 0000000..2315ef1 --- /dev/null +++ b/examples/magickey_auth/main.py @@ -0,0 +1,29 @@ +from fasthtml.common import * +from plash_cli.auth import * + +app, rt = fast_app() + +@rt +def index(session): + if email:=session.get('email'): + return (H1(f"Welcome! You are logged in as {email}"), + A("Logout", href="/logout")) + else: + return (H1("Welcome! Please sign in."), + A("Sign in with Email", href=mk_magickey_signin_url(session))) + +@rt(signin_completed_rt) +def signin_completed(session, signin_reply: str): + try: + email = email_from_magickey_reply(session, signin_reply) + session['email'] = email + return RedirectResponse('/', status_code=303) + except PlashAuthError as e: + return Div(H2("Login Failed"), P(f"Error: {e}"), A("Try Again", href="/")) + +@rt('/logout') +def logout(session): + session.pop('email') + return RedirectResponse('/', status_code=303) + +serve() diff --git a/examples/magickey_auth/requirements.txt b/examples/magickey_auth/requirements.txt new file mode 100644 index 0000000..0ad3fc1 --- /dev/null +++ b/examples/magickey_auth/requirements.txt @@ -0,0 +1,2 @@ +plash_cli @ git+https://github.com/AnswerDotAI/plash_cli@magickey +python-fasthtml diff --git a/nbs/01_auth.ipynb b/nbs/01_auth.ipynb index 9eb2c3d..ddbf0ca 100644 --- a/nbs/01_auth.ipynb +++ b/nbs/01_auth.ipynb @@ -10,12 +10,20 @@ "> Client side logic to add Plash Auth to your app" ] }, + { + "cell_type": "markdown", + "id": "4416a9b8", + "metadata": {}, + "source": [ + "## Google Auth" + ] + }, { "cell_type": "markdown", "id": "188fb79d", "metadata": {}, "source": [ - "This page describes how Plash Auth is implemented client side. \n", + "This section describes how Plash Auth implements Google Auth client side. \n", "\n", "Please see the [how to](how_to/auth.html) for instructions on how to use it." ] @@ -25,7 +33,7 @@ "id": "59ce8def", "metadata": {}, "source": [ - "## Setup -" + "### Setup -" ] }, { @@ -116,8 +124,8 @@ "source": [ "#| export\n", "def mk_signin_url(session: dict, # Session dictionary\n", - " email_re: str=None, # Regex filter for allowed email addresses\n", - " hd_re: str=None): # Regex filter for allowed Google hosted domains\n", + " email_re: str=None, # Regex filter for allowed email addresses\n", + " hd_re: str=None): # Regex filter for allowed Google hosted domains\n", " \"Generate a Google Sign-In URL for Plash authentication.\"\n", " if not _in_prod: return f\"{signin_completed_rt}?signin_reply=mock-sign-in-reply\"\n", " res = _signin_url(email_re, hd_re)\n", @@ -216,6 +224,76 @@ "When testing locally this will always return the mock Google ID `'424242424242424242424'`." ] }, + { + "cell_type": "markdown", + "id": "ba560a3a", + "metadata": {}, + "source": [ + "## MagicKey" + ] + }, + { + "cell_type": "markdown", + "id": "1c7b6cd6", + "metadata": {}, + "source": [ + "This section shows how Plash Auth implements magic links + passkeys as a login method." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "011996f6", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def _magickey_url(email_re=None, hd_re=None):\n", + " # TODO refactor\n", + " url = os.environ['PLASH_AUTH_URL'].replace('request_signin', 'request_magickey')\n", + " res = httpx.post(url, json=dict(email_re=email_re, hd_re=hd_re),\n", + " auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']),\n", + " headers={'X-PLASH-AUTH-VERSION': __version__}).raise_for_status().json()\n", + " if \"warning\" in res: warn(res.pop('warning'))\n", + " return res" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4cb061d7", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def mk_magickey_signin_url(session: dict, # Session dictionary\n", + " email_re: str=None, # Regex filter for allowed email addresses\n", + " hd_re: str=None): # Regex filter for allowed hosted domains\n", + " \"Generate a MagicKey Sign-In URL for Plash authentication.\"\n", + " if not _in_prod: return f\"{signin_completed_rt}?signin_reply=mock-sign-in-reply\"\n", + " res = _magickey_url(email_re, hd_re)\n", + " session['req_id'] = res['req_id']\n", + " return res['plash_magickey_url']\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ecbd0e66", + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def email_from_magickey_reply(session: dict, # Session dictionary containing 'req_id'\n", + " reply: str): # The JWT reply string from Plash after MagicKey authentication\n", + " \"Validate MagicKey sign-in reply and return email address if valid.\"\n", + " if not _in_prod: return 'test@example.com'\n", + " parsed = _parse_jwt(reply)\n", + " if session.get('req_id') != parsed['req_id']: raise PlashAuthError(\"Request originated from a different browser than the one receiving the reply\")\n", + " if parsed['err']: raise PlashAuthError(f\"Authentication failed: {parsed['err']}\")\n", + " return parsed['sub']\n" + ] + }, { "cell_type": "markdown", "id": "72eaabaa", @@ -238,11 +316,8 @@ } ], "metadata": { - "kernelspec": { - "display_name": "python3", - "language": "python", - "name": "python3" - } + "solveit_dialog_mode": "learning", + "solveit_ver": 2 }, "nbformat": 4, "nbformat_minor": 5 diff --git a/plash_cli/_modidx.py b/plash_cli/_modidx.py index 726c355..81801b3 100644 --- a/plash_cli/_modidx.py +++ b/plash_cli/_modidx.py @@ -6,9 +6,12 @@ 'git_url': 'https://github.com/AnswerDotAI/plash_cli', 'lib_path': 'plash_cli'}, 'syms': { 'plash_cli.auth': { 'plash_cli.auth.PlashAuthError': ('auth.html#plashautherror', 'plash_cli/auth.py'), + 'plash_cli.auth._magickey_url': ('auth.html#_magickey_url', 'plash_cli/auth.py'), 'plash_cli.auth._parse_jwt': ('auth.html#_parse_jwt', 'plash_cli/auth.py'), 'plash_cli.auth._signin_url': ('auth.html#_signin_url', 'plash_cli/auth.py'), + 'plash_cli.auth.email_from_magickey_reply': ('auth.html#email_from_magickey_reply', 'plash_cli/auth.py'), 'plash_cli.auth.goog_id_from_signin_reply': ('auth.html#goog_id_from_signin_reply', 'plash_cli/auth.py'), + 'plash_cli.auth.mk_magickey_signin_url': ('auth.html#mk_magickey_signin_url', 'plash_cli/auth.py'), 'plash_cli.auth.mk_signin_url': ('auth.html#mk_signin_url', 'plash_cli/auth.py')}, 'plash_cli.cli': { 'plash_cli.cli.Path._is_dir_empty': ('cli.html#path._is_dir_empty', 'plash_cli/cli.py'), 'plash_cli.cli.PlashError': ('cli.html#plasherror', 'plash_cli/cli.py'), diff --git a/plash_cli/auth.py b/plash_cli/auth.py index 511fbf9..52e369f 100644 --- a/plash_cli/auth.py +++ b/plash_cli/auth.py @@ -3,7 +3,8 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_auth.ipynb. # %% auto #0 -__all__ = ['signin_completed_rt', 'mk_signin_url', 'PlashAuthError', 'goog_id_from_signin_reply'] +__all__ = ['signin_completed_rt', 'mk_signin_url', 'PlashAuthError', 'goog_id_from_signin_reply', 'mk_magickey_signin_url', + 'email_from_magickey_reply'] # %% ../nbs/01_auth.ipynb #c6aa552a import httpx,os,jwt @@ -28,8 +29,8 @@ def _signin_url(email_re: str=None, hd_re: str=None): # %% ../nbs/01_auth.ipynb #74a0a24d def mk_signin_url(session: dict, # Session dictionary - email_re: str=None, # Regex filter for allowed email addresses - hd_re: str=None): # Regex filter for allowed Google hosted domains + email_re: str=None, # Regex filter for allowed email addresses + hd_re: str=None): # Regex filter for allowed Google hosted domains "Generate a Google Sign-In URL for Plash authentication." if not _in_prod: return f"{signin_completed_rt}?signin_reply=mock-sign-in-reply" res = _signin_url(email_re, hd_re) @@ -58,3 +59,35 @@ def goog_id_from_signin_reply(session: dict, # Session dictionary containing 're if session.get('req_id') != parsed['req_id']: raise PlashAuthError("Request originated from a different browser than the one receiving the reply") if parsed['err']: raise PlashAuthError(f"Authentication failed: {parsed['err']}") return parsed['sub'] + +# %% ../nbs/01_auth.ipynb #011996f6 +def _magickey_url(email_re=None, hd_re=None): + # TODO refactor + url = os.environ['PLASH_AUTH_URL'].replace('request_signin', 'request_magickey') + res = httpx.post(url, json=dict(email_re=email_re, hd_re=hd_re), + auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']), + headers={'X-PLASH-AUTH-VERSION': __version__}).raise_for_status().json() + if "warning" in res: warn(res.pop('warning')) + return res + +# %% ../nbs/01_auth.ipynb #4cb061d7 +def mk_magickey_signin_url(session: dict, # Session dictionary + email_re: str=None, # Regex filter for allowed email addresses + hd_re: str=None): # Regex filter for allowed hosted domains + "Generate a MagicKey Sign-In URL for Plash authentication." + if not _in_prod: return f"{signin_completed_rt}?signin_reply=mock-sign-in-reply" + res = _magickey_url(email_re, hd_re) + session['req_id'] = res['req_id'] + return res['plash_magickey_url'] + + +# %% ../nbs/01_auth.ipynb #ecbd0e66 +def email_from_magickey_reply(session: dict, # Session dictionary containing 'req_id' + reply: str): # The JWT reply string from Plash after MagicKey authentication + "Validate MagicKey sign-in reply and return email address if valid." + if not _in_prod: return 'test@example.com' + parsed = _parse_jwt(reply) + if session.get('req_id') != parsed['req_id']: raise PlashAuthError("Request originated from a different browser than the one receiving the reply") + if parsed['err']: raise PlashAuthError(f"Authentication failed: {parsed['err']}") + return parsed['sub'] + From 117585db7a562a26b0a0b02924a1bbc27125b27e Mon Sep 17 00:00:00 2001 From: Rens Date: Wed, 15 Apr 2026 15:07:00 +0200 Subject: [PATCH 2/5] magiclink only --- nbs/01_auth.ipynb | 60 +++++++++----------------------------------- plash_cli/_modidx.py | 6 ++--- plash_cli/auth.py | 45 ++++++++------------------------- 3 files changed, 25 insertions(+), 86 deletions(-) diff --git a/nbs/01_auth.ipynb b/nbs/01_auth.ipynb index ddbf0ca..21e2d0e 100644 --- a/nbs/01_auth.ipynb +++ b/nbs/01_auth.ipynb @@ -81,7 +81,7 @@ "source": [ "#| export\n", "def _signin_url(email_re: str=None, hd_re: str=None):\n", - " res = httpx.post(os.environ['PLASH_AUTH_URL'], json=dict(email_re=email_re, hd_re=hd_re), \n", + " res = httpx.post(os.environ['PLASH_AUTH_URL']+'/request_signin', json=dict(email_re=email_re, hd_re=hd_re), \n", " auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']), \n", " headers={'X-PLASH-AUTH-VERSION': __version__}).raise_for_status().json()\n", " if \"warning\" in res: warn(res.pop('warning'))\n", @@ -229,7 +229,7 @@ "id": "ba560a3a", "metadata": {}, "source": [ - "## MagicKey" + "## Magic Link" ] }, { @@ -237,61 +237,25 @@ "id": "1c7b6cd6", "metadata": {}, "source": [ - "This section shows how Plash Auth implements magic links + passkeys as a login method." + "Pla.sh provides a service where you can send magic links for sign-up or login to your users for free. " ] }, { "cell_type": "code", "execution_count": null, - "id": "011996f6", + "id": "ad5693c9", "metadata": {}, "outputs": [], "source": [ "#| export\n", - "def _magickey_url(email_re=None, hd_re=None):\n", - " # TODO refactor\n", - " url = os.environ['PLASH_AUTH_URL'].replace('request_signin', 'request_magickey')\n", - " res = httpx.post(url, json=dict(email_re=email_re, hd_re=hd_re),\n", - " auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']),\n", - " headers={'X-PLASH-AUTH-VERSION': __version__}).raise_for_status().json()\n", - " if \"warning\" in res: warn(res.pop('warning'))\n", - " return res" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4cb061d7", - "metadata": {}, - "outputs": [], - "source": [ - "#| export\n", - "def mk_magickey_signin_url(session: dict, # Session dictionary\n", - " email_re: str=None, # Regex filter for allowed email addresses\n", - " hd_re: str=None): # Regex filter for allowed hosted domains\n", - " \"Generate a MagicKey Sign-In URL for Plash authentication.\"\n", - " if not _in_prod: return f\"{signin_completed_rt}?signin_reply=mock-sign-in-reply\"\n", - " res = _magickey_url(email_re, hd_re)\n", - " session['req_id'] = res['req_id']\n", - " return res['plash_magickey_url']\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ecbd0e66", - "metadata": {}, - "outputs": [], - "source": [ - "#| export\n", - "def email_from_magickey_reply(session: dict, # Session dictionary containing 'req_id'\n", - " reply: str): # The JWT reply string from Plash after MagicKey authentication\n", - " \"Validate MagicKey sign-in reply and return email address if valid.\"\n", - " if not _in_prod: return 'test@example.com'\n", - " parsed = _parse_jwt(reply)\n", - " if session.get('req_id') != parsed['req_id']: raise PlashAuthError(\"Request originated from a different browser than the one receiving the reply\")\n", - " if parsed['err']: raise PlashAuthError(f\"Authentication failed: {parsed['err']}\")\n", - " return parsed['sub']\n" + "def send_magiclink(email: str, # Email address to send magic link to\n", + " url: str): # Magic link URL (must match app's domain)\n", + " \"Send a magic link email to the given address via Plash Auth.\"\n", + " httpx.post(os.environ['PLASH_AUTH_URL']+'/send_magiclink',\n", + " json=dict(email=email, url=url),\n", + " auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']),\n", + " headers={'X-PLASH-AUTH-VERSION': __version__}\n", + " ).raise_for_status()" ] }, { diff --git a/plash_cli/_modidx.py b/plash_cli/_modidx.py index 81801b3..41a3f2f 100644 --- a/plash_cli/_modidx.py +++ b/plash_cli/_modidx.py @@ -6,13 +6,11 @@ 'git_url': 'https://github.com/AnswerDotAI/plash_cli', 'lib_path': 'plash_cli'}, 'syms': { 'plash_cli.auth': { 'plash_cli.auth.PlashAuthError': ('auth.html#plashautherror', 'plash_cli/auth.py'), - 'plash_cli.auth._magickey_url': ('auth.html#_magickey_url', 'plash_cli/auth.py'), 'plash_cli.auth._parse_jwt': ('auth.html#_parse_jwt', 'plash_cli/auth.py'), 'plash_cli.auth._signin_url': ('auth.html#_signin_url', 'plash_cli/auth.py'), - 'plash_cli.auth.email_from_magickey_reply': ('auth.html#email_from_magickey_reply', 'plash_cli/auth.py'), 'plash_cli.auth.goog_id_from_signin_reply': ('auth.html#goog_id_from_signin_reply', 'plash_cli/auth.py'), - 'plash_cli.auth.mk_magickey_signin_url': ('auth.html#mk_magickey_signin_url', 'plash_cli/auth.py'), - 'plash_cli.auth.mk_signin_url': ('auth.html#mk_signin_url', 'plash_cli/auth.py')}, + 'plash_cli.auth.mk_signin_url': ('auth.html#mk_signin_url', 'plash_cli/auth.py'), + 'plash_cli.auth.send_magiclink': ('auth.html#send_magiclink', 'plash_cli/auth.py')}, 'plash_cli.cli': { 'plash_cli.cli.Path._is_dir_empty': ('cli.html#path._is_dir_empty', 'plash_cli/cli.py'), 'plash_cli.cli.PlashError': ('cli.html#plasherror', 'plash_cli/cli.py'), 'plash_cli.cli._app_list': ('cli.html#_app_list', 'plash_cli/cli.py'), diff --git a/plash_cli/auth.py b/plash_cli/auth.py index 52e369f..375570b 100644 --- a/plash_cli/auth.py +++ b/plash_cli/auth.py @@ -3,8 +3,7 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_auth.ipynb. # %% auto #0 -__all__ = ['signin_completed_rt', 'mk_signin_url', 'PlashAuthError', 'goog_id_from_signin_reply', 'mk_magickey_signin_url', - 'email_from_magickey_reply'] +__all__ = ['signin_completed_rt', 'mk_signin_url', 'PlashAuthError', 'goog_id_from_signin_reply', 'send_magiclink'] # %% ../nbs/01_auth.ipynb #c6aa552a import httpx,os,jwt @@ -18,7 +17,7 @@ # %% ../nbs/01_auth.ipynb #e6590075 def _signin_url(email_re: str=None, hd_re: str=None): - res = httpx.post(os.environ['PLASH_AUTH_URL'], json=dict(email_re=email_re, hd_re=hd_re), + res = httpx.post(os.environ['PLASH_AUTH_URL']+'/request_signin', json=dict(email_re=email_re, hd_re=hd_re), auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']), headers={'X-PLASH-AUTH-VERSION': __version__}).raise_for_status().json() if "warning" in res: warn(res.pop('warning')) @@ -60,34 +59,12 @@ def goog_id_from_signin_reply(session: dict, # Session dictionary containing 're if parsed['err']: raise PlashAuthError(f"Authentication failed: {parsed['err']}") return parsed['sub'] -# %% ../nbs/01_auth.ipynb #011996f6 -def _magickey_url(email_re=None, hd_re=None): - # TODO refactor - url = os.environ['PLASH_AUTH_URL'].replace('request_signin', 'request_magickey') - res = httpx.post(url, json=dict(email_re=email_re, hd_re=hd_re), - auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']), - headers={'X-PLASH-AUTH-VERSION': __version__}).raise_for_status().json() - if "warning" in res: warn(res.pop('warning')) - return res - -# %% ../nbs/01_auth.ipynb #4cb061d7 -def mk_magickey_signin_url(session: dict, # Session dictionary - email_re: str=None, # Regex filter for allowed email addresses - hd_re: str=None): # Regex filter for allowed hosted domains - "Generate a MagicKey Sign-In URL for Plash authentication." - if not _in_prod: return f"{signin_completed_rt}?signin_reply=mock-sign-in-reply" - res = _magickey_url(email_re, hd_re) - session['req_id'] = res['req_id'] - return res['plash_magickey_url'] - - -# %% ../nbs/01_auth.ipynb #ecbd0e66 -def email_from_magickey_reply(session: dict, # Session dictionary containing 'req_id' - reply: str): # The JWT reply string from Plash after MagicKey authentication - "Validate MagicKey sign-in reply and return email address if valid." - if not _in_prod: return 'test@example.com' - parsed = _parse_jwt(reply) - if session.get('req_id') != parsed['req_id']: raise PlashAuthError("Request originated from a different browser than the one receiving the reply") - if parsed['err']: raise PlashAuthError(f"Authentication failed: {parsed['err']}") - return parsed['sub'] - +# %% ../nbs/01_auth.ipynb #ad5693c9 +def send_magiclink(email: str, # Email address to send magic link to + url: str): # Magic link URL (must match app's domain) + "Send a magic link email to the given address via Plash Auth." + httpx.post(os.environ['PLASH_AUTH_URL']+'/send_magiclink', + json=dict(email=email, url=url), + auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']), + headers={'X-PLASH-AUTH-VERSION': __version__} + ).raise_for_status() From 05ebc82bfcc1c590dfe3fe1a9390dd6343ee71d2 Mon Sep 17 00:00:00 2001 From: Rens Date: Wed, 15 Apr 2026 15:23:20 +0200 Subject: [PATCH 3/5] fix return statement in magiclink --- nbs/01_auth.ipynb | 4 ++-- plash_cli/auth.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nbs/01_auth.ipynb b/nbs/01_auth.ipynb index 21e2d0e..4bc0607 100644 --- a/nbs/01_auth.ipynb +++ b/nbs/01_auth.ipynb @@ -251,11 +251,11 @@ "def send_magiclink(email: str, # Email address to send magic link to\n", " url: str): # Magic link URL (must match app's domain)\n", " \"Send a magic link email to the given address via Plash Auth.\"\n", - " httpx.post(os.environ['PLASH_AUTH_URL']+'/send_magiclink',\n", + " return httpx.post(os.environ['PLASH_AUTH_URL']+'/send_magiclink',\n", " json=dict(email=email, url=url),\n", " auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']),\n", " headers={'X-PLASH-AUTH-VERSION': __version__}\n", - " ).raise_for_status()" + " )" ] }, { diff --git a/plash_cli/auth.py b/plash_cli/auth.py index 375570b..a54782d 100644 --- a/plash_cli/auth.py +++ b/plash_cli/auth.py @@ -63,8 +63,8 @@ def goog_id_from_signin_reply(session: dict, # Session dictionary containing 're def send_magiclink(email: str, # Email address to send magic link to url: str): # Magic link URL (must match app's domain) "Send a magic link email to the given address via Plash Auth." - httpx.post(os.environ['PLASH_AUTH_URL']+'/send_magiclink', + return httpx.post(os.environ['PLASH_AUTH_URL']+'/send_magiclink', json=dict(email=email, url=url), auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']), headers={'X-PLASH-AUTH-VERSION': __version__} - ).raise_for_status() + ) From c6cb4fdb1edec13659f23ac694b098cd92ae9ec8 Mon Sep 17 00:00:00 2001 From: Rens Date: Wed, 15 Apr 2026 20:02:53 +0200 Subject: [PATCH 4/5] update example --- examples/magickey_auth/main.py | 75 +++++++++++++++++-------- examples/magickey_auth/requirements.txt | 4 +- 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/examples/magickey_auth/main.py b/examples/magickey_auth/main.py index 2315ef1..f5b7093 100644 --- a/examples/magickey_auth/main.py +++ b/examples/magickey_auth/main.py @@ -1,29 +1,58 @@ from fasthtml.common import * -from plash_cli.auth import * +from fasthtml.magickey import MagicKey +from plash_cli.auth import send_magiclink + +db = database('data/data.db') +users = db.t.user.create(id=int, email=str, pk='id', if_not_exists=True) +passkeys = db.t.passkey.create(id=str, user_id=int, public_key=bytes, sign_count=int, pk='id', if_not_exists=True) +User,Passkey = users.dataclass(),passkeys.dataclass() + +class Auth(MagicKey): + def get_user_id(self, email): + res = users(where="email = ?", where_args=[email]) + if res: return res[0].id + return users.insert(User(email=email)).id + def has_passkey(self, email): + return bool(passkeys(where="user_id = ?", where_args=[self.get_user_id(email)])) + def get_passkey(self, cred_id): + try: r = passkeys[cred_id] + except NotFoundError: return None + return dict(email=users[r.user_id].email, public_key=r.public_key, sign_count=r.sign_count) + def save_passkey(self, cred_id, email, public_key, sign_count): + uid = self.get_user_id(email) + passkeys.insert(Passkey(id=cred_id, user_id=uid, public_key=public_key, sign_count=sign_count)) + def update_passkey(self, cred_id, sign_count): + passkeys.update(Passkey(id=cred_id, sign_count=sign_count)) + +def send_email(email, url): + res = send_magiclink(email,url) + if res.status_code == 200: return P(f'Your magic login link has beent sent to: {email}.') + else: return P('Something went wrong, try again later.') app, rt = fast_app() +mk = Auth(app, send_email=send_email) + +@rt('/') +def home(auth): + u = users[auth] + return P(f'Hello {u.email}!'), A('Log out', href='/logout') + +@rt('/login') +def login(error: str=None): + errmsg = P(error.replace('_', ' ').title(), style='color:red') if error else '' + return Titled('Sign In', errmsg, + Button('Sign in with Passkey', hx_post='/request_passkey_auth', target_id='scripts'), + Hr(), + Form(method='post', action='/send_magic_link')( + Input(name='email', type='email', placeholder='you@example.com'), + Button('Send Magic Link')), + Div(id='scripts')) -@rt -def index(session): - if email:=session.get('email'): - return (H1(f"Welcome! You are logged in as {email}"), - A("Logout", href="/logout")) - else: - return (H1("Welcome! Please sign in."), - A("Sign in with Email", href=mk_magickey_signin_url(session))) - -@rt(signin_completed_rt) -def signin_completed(session, signin_reply: str): - try: - email = email_from_magickey_reply(session, signin_reply) - session['email'] = email - return RedirectResponse('/', status_code=303) - except PlashAuthError as e: - return Div(H2("Login Failed"), P(f"Error: {e}"), A("Try Again", href="/")) - -@rt('/logout') -def logout(session): - session.pop('email') - return RedirectResponse('/', status_code=303) +@rt('/setup_passkey') +def setup_passkey(): return Titled('Set Up Passkey', + P('Set up a passkey for faster logins next time?'), + Button('Register Passkey', hx_post='/request_passkey_reg', target_id='scripts'), + Form(Button('Skip'), method='post', action='/skip_passkey_reg'), + Div(id='scripts')) serve() diff --git a/examples/magickey_auth/requirements.txt b/examples/magickey_auth/requirements.txt index 0ad3fc1..25c8d0b 100644 --- a/examples/magickey_auth/requirements.txt +++ b/examples/magickey_auth/requirements.txt @@ -1,2 +1,4 @@ plash_cli @ git+https://github.com/AnswerDotAI/plash_cli@magickey -python-fasthtml +python-fasthtml @ git+https://github.com/AnswerDotAI/fasthtml@magickey +webauthn +numpy \ No newline at end of file From 0af32e41237ad19e85864b7fdbcc13adae6cc98a Mon Sep 17 00:00:00 2001 From: Rens Date: Thu, 16 Apr 2026 09:00:41 +0200 Subject: [PATCH 5/5] update table creation --- examples/magickey_auth/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/magickey_auth/main.py b/examples/magickey_auth/main.py index f5b7093..86798c3 100644 --- a/examples/magickey_auth/main.py +++ b/examples/magickey_auth/main.py @@ -3,9 +3,10 @@ from plash_cli.auth import send_magiclink db = database('data/data.db') -users = db.t.user.create(id=int, email=str, pk='id', if_not_exists=True) -passkeys = db.t.passkey.create(id=str, user_id=int, public_key=bytes, sign_count=int, pk='id', if_not_exists=True) -User,Passkey = users.dataclass(),passkeys.dataclass() +class User: id:int; email:str +class Passkey: id:str; user_id:int; public_key:bytes; sign_count:int +users = db.create(User) +passkeys = db.create(Passkey) class Auth(MagicKey): def get_user_id(self, email):