Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions examples/magickey_auth/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from fasthtml.common 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need if_not_exists? Also do you need to import database from fastlite?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do need it because otherwise on a second start when the database already exists it raises SQLError: table [user] already exists. I double checked the fasthtml docs and did find there's a different suggested pattern so I've moved to use that instead:

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)

Re database: it's also exported from fasthtml.common so we technically dont need to include it in the imports. do you prefer we'd add it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the approach here is neater for fastlite stuff: https://github.com/AnswerDotAI/fasthtml/blob/main/examples/adv_app_strip.py

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woops, missed this comment, but I got to the same solution in the other thread. Thanks!


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('/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()
4 changes: 4 additions & 0 deletions examples/magickey_auth/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
plash_cli @ git+https://github.com/AnswerDotAI/plash_cli@magickey
python-fasthtml @ git+https://github.com/AnswerDotAI/fasthtml@magickey
webauthn
numpy
59 changes: 49 additions & 10 deletions nbs/01_auth.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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."
]
Expand All @@ -25,7 +33,7 @@
"id": "59ce8def",
"metadata": {},
"source": [
"## Setup -"
"### Setup -"
]
},
{
Expand Down Expand Up @@ -73,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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -216,6 +224,40 @@
"When testing locally this will always return the mock Google ID `'424242424242424242424'`."
]
},
{
"cell_type": "markdown",
"id": "ba560a3a",
"metadata": {},
"source": [
"## Magic Link"
]
},
{
"cell_type": "markdown",
"id": "1c7b6cd6",
"metadata": {},
"source": [
"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": "ad5693c9",
"metadata": {},
"outputs": [],
"source": [
"#| export\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",
" 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",
" )"
]
},
{
"cell_type": "markdown",
"id": "72eaabaa",
Expand All @@ -238,11 +280,8 @@
}
],
"metadata": {
"kernelspec": {
"display_name": "python3",
"language": "python",
"name": "python3"
}
"solveit_dialog_mode": "learning",
"solveit_ver": 2
},
"nbformat": 4,
"nbformat_minor": 5
Expand Down
3 changes: 2 additions & 1 deletion plash_cli/_modidx.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
'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.goog_id_from_signin_reply': ('auth.html#goog_id_from_signin_reply', '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'),
Expand Down
18 changes: 14 additions & 4 deletions plash_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +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']
__all__ = ['signin_completed_rt', 'mk_signin_url', 'PlashAuthError', 'goog_id_from_signin_reply', 'send_magiclink']

# %% ../nbs/01_auth.ipynb #c6aa552a
import httpx,os,jwt
Expand All @@ -17,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'))
Expand All @@ -28,8 +28,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)
Expand Down Expand Up @@ -58,3 +58,13 @@ 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 #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."
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__}
)
Loading