Skip to content

Commit 800242a

Browse files
scotttrinhfantix
andcommitted
Add auth extension handling
Co-authored-by: Fantix King <[email protected]>
1 parent 61d652e commit 800242a

23 files changed

+1120
-95
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from __future__ import annotations
2+
from typing import Annotated
3+
4+
import json
5+
import logging
6+
from http import HTTPStatus
7+
8+
from fastapi import APIRouter, Depends, Form
9+
from fastapi.responses import RedirectResponse
10+
from gel_auth_fastapi import (
11+
make_email_password,
12+
email_password as core_email_password,
13+
)
14+
15+
from .config import BASE_URL
16+
from .gel_client import client
17+
from .queries import create_user_async_edgeql as create_user_qry
18+
19+
logger = logging.getLogger("fast_jelly")
20+
router = APIRouter()
21+
email_password = make_email_password(
22+
client,
23+
verify_url=f"{BASE_URL}/auth/verify",
24+
reset_url=f"{BASE_URL}/ui/reset-password",
25+
)
26+
27+
28+
@router.post(
29+
"/auth/register",
30+
response_class=RedirectResponse,
31+
status_code=HTTPStatus.SEE_OTHER,
32+
)
33+
async def register(
34+
email: Annotated[str, Form()],
35+
sign_up_response: Annotated[
36+
core_email_password.SignUpResponse, Depends(email_password.handle_sign_up)
37+
],
38+
):
39+
if not isinstance(sign_up_response, core_email_password.SignUpFailedResponse):
40+
user = await create_user_qry.create_user(
41+
client,
42+
name=email,
43+
identity_id=sign_up_response.identity_id,
44+
)
45+
print(f"Created user: {json.dumps(user, default=str)}")
46+
47+
match sign_up_response:
48+
case core_email_password.SignUpCompleteResponse():
49+
return "/"
50+
case core_email_password.SignUpVerificationRequiredResponse():
51+
return "/signin?incomplete=verification_required"
52+
case core_email_password.SignUpFailedResponse():
53+
logger.error(f"Sign up failed: {sign_up_response}")
54+
return "/signin?error=failure"
55+
case _:
56+
raise Exception("Invalid sign up response")
57+
58+
59+
@router.post(
60+
"/auth/authenticate",
61+
response_class=RedirectResponse,
62+
status_code=HTTPStatus.SEE_OTHER,
63+
)
64+
async def authenticate(
65+
sign_in_response: Annotated[
66+
core_email_password.SignInResponse, Depends(email_password.handle_sign_in)
67+
],
68+
):
69+
match sign_in_response:
70+
case core_email_password.SignInCompleteResponse():
71+
return "/"
72+
case core_email_password.SignInVerificationRequiredResponse():
73+
return "/signin?incomplete=verification_required"
74+
case core_email_password.SignInFailedResponse():
75+
logger.error(f"Sign in failed: {sign_in_response}")
76+
return "/signin?error=failure"
77+
case _:
78+
raise Exception("Invalid sign in response")
79+
80+
81+
@router.get(
82+
"/auth/verify",
83+
response_class=RedirectResponse,
84+
status_code=HTTPStatus.SEE_OTHER,
85+
)
86+
async def verify(
87+
verify_response: Annotated[
88+
core_email_password.EmailVerificationResponse,
89+
Depends(email_password.handle_verify_email),
90+
],
91+
):
92+
match verify_response:
93+
case core_email_password.EmailVerificationCompleteResponse():
94+
return "/"
95+
case core_email_password.EmailVerificationMissingProofResponse():
96+
return "/signin?incomplete=verify"
97+
case core_email_password.EmailVerificationFailedResponse():
98+
logger.error(f"Verify email failed: {verify_response}")
99+
return "/signin?error=failure"
100+
case _:
101+
raise Exception("Invalid verify email response")
102+
103+
104+
@router.post(
105+
"/auth/send-password-reset",
106+
response_class=RedirectResponse,
107+
status_code=HTTPStatus.SEE_OTHER,
108+
)
109+
async def send_password_reset(
110+
send_password_reset_response: Annotated[
111+
core_email_password.SendPasswordResetEmailResponse,
112+
Depends(email_password.handle_send_password_reset),
113+
],
114+
):
115+
match send_password_reset_response:
116+
case core_email_password.SendPasswordResetEmailCompleteResponse():
117+
return "/signin?incomplete=password_reset_sent"
118+
case core_email_password.SendPasswordResetEmailFailedResponse():
119+
logger.error(f"Send password reset failed: {send_password_reset_response}")
120+
return "/signin?error=failure"
121+
case _:
122+
raise Exception("Invalid send password reset response")
123+
124+
125+
@router.post(
126+
"/auth/reset-password",
127+
response_class=RedirectResponse,
128+
status_code=HTTPStatus.SEE_OTHER,
129+
)
130+
async def reset_password(
131+
reset_password_response: Annotated[
132+
core_email_password.PasswordResetResponse,
133+
Depends(email_password.handle_reset_password),
134+
],
135+
):
136+
match reset_password_response:
137+
case core_email_password.PasswordResetCompleteResponse():
138+
return "/"
139+
case core_email_password.PasswordResetMissingProofResponse():
140+
return "/signin?incomplete=reset_password"
141+
case core_email_password.PasswordResetFailedResponse():
142+
logger.error(f"Reset password failed: {reset_password_response}")
143+
return "/signin?error=failure"
144+
case _:
145+
raise Exception("Invalid reset password response")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import os
2+
3+
4+
APP_HOST = os.getenv("APP_HOST", default="localhost")
5+
APP_PORT = os.getenv("APP_PORT", default="8000")
6+
BASE_URL = f"http://{APP_HOST}:{APP_PORT}"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import asyncio
2+
import os
3+
import secrets
4+
5+
from gel import create_async_client
6+
7+
8+
async def main():
9+
client = create_async_client()
10+
11+
auth_signing_key = os.getenv("GEL_AUTH_SIGNING_KEY", secrets.token_urlsafe(32))
12+
13+
await client.execute(
14+
f"""
15+
configure current branch reset ext::auth::AuthConfig;
16+
configure current branch reset ext::auth::ProviderConfig;
17+
configure current branch reset ext::auth::EmailPasswordProviderConfig;
18+
configure current branch reset cfg::EmailProviderConfig;
19+
20+
configure current branch set
21+
ext::auth::AuthConfig::auth_signing_key := "{auth_signing_key}";
22+
23+
configure current branch set
24+
ext::auth::AuthConfig::app_name := "Fast Jelly";
25+
26+
configure current branch set
27+
ext::auth::AuthConfig::allowed_redirect_urls := {{"http://localhost:8000"}};
28+
29+
configure current branch insert
30+
ext::auth::EmailPasswordProviderConfig {{
31+
require_verification := true,
32+
}};
33+
34+
configure current branch insert
35+
cfg::SMTPProviderConfig {{
36+
name := "mailpit",
37+
host := "localhost",
38+
port := 1025,
39+
username := "smtpuser",
40+
password := "smtppassword",
41+
sender := "[email protected]",
42+
validate_certs := false,
43+
}};
44+
45+
configure current branch set
46+
cfg::current_email_provider_name := "mailpit";
47+
"""
48+
)
49+
50+
51+
if __name__ == "__main__":
52+
asyncio.run(main())

python-fastapi/examples/basic-async/app/events.py

+10-9
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
from __future__ import annotations
22

3-
import edgedb
43
import datetime
5-
64
from http import HTTPStatus
7-
from typing import List
8-
from fastapi import APIRouter, HTTPException, Query
5+
6+
import gel
7+
from fastapi import APIRouter, HTTPException
8+
from gel_auth_fastapi import SessionDep
99
from pydantic import BaseModel
1010

1111
from .queries import (
1212
create_event_async_edgeql as create_event_qry,
1313
)
1414

15-
1615
router = APIRouter()
17-
client = edgedb.create_async_client()
1816

1917

2018
class RequestData(BaseModel):
@@ -25,7 +23,10 @@ class RequestData(BaseModel):
2523

2624

2725
@router.post("/events", status_code=HTTPStatus.CREATED)
28-
async def post_event(event: RequestData) -> create_event_qry.CreateEventResult:
26+
async def post_event(
27+
event: RequestData, session: SessionDep
28+
) -> create_event_qry.CreateEventResult:
29+
client = session.client
2930
try:
3031
created_event = await create_event_qry.create_event(
3132
client,
@@ -34,13 +35,13 @@ async def post_event(event: RequestData) -> create_event_qry.CreateEventResult:
3435
schedule=event.schedule,
3536
host_name=event.host_name,
3637
)
37-
except edgedb.errors.InvalidValueError as ex:
38+
except gel.errors.InvalidValueError as ex:
3839
raise HTTPException(
3940
status_code=HTTPStatus.BAD_REQUEST,
4041
detail={"error": str(ex)},
4142
)
4243

43-
except edgedb.errors.ConstraintViolationError:
44+
except gel.errors.ConstraintViolationError:
4445
raise HTTPException(
4546
status_code=HTTPStatus.BAD_REQUEST,
4647
detail={"error": "Event '{event.name}' already exists"},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import gel
2+
3+
4+
client = gel.create_async_client()
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
11
from __future__ import annotations
22

3-
from fastapi import FastAPI
4-
from starlette.middleware.cors import CORSMiddleware
3+
import logging
4+
import sys
55

6-
from app import users, events
6+
from fastapi import FastAPI, APIRouter
77

8+
from app import auth, users, events, ui
9+
10+
11+
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
12+
stream_handler = logging.StreamHandler(sys.stdout)
13+
stream_handler.setFormatter(formatter)
14+
15+
logger = logging.getLogger("fast_jelly")
16+
logger.setLevel(logging.DEBUG)
17+
logger.addHandler(stream_handler)
18+
19+
auth_core_logger = logging.getLogger("gel_auth_core")
20+
auth_core_logger.setLevel(logging.DEBUG)
21+
auth_core_logger.addHandler(stream_handler)
822

923
fast_api = FastAPI()
24+
fast_api.include_router(ui.router)
25+
fast_api.include_router(auth.router)
1026

11-
fast_api.add_middleware(
12-
CORSMiddleware,
13-
allow_origins=["*"],
14-
allow_credentials=True,
15-
allow_methods=["*"],
16-
allow_headers=["*"],
17-
)
27+
api_router = APIRouter()
28+
api_router.include_router(users.router)
29+
api_router.include_router(events.router)
1830

19-
fast_api.include_router(users.router)
20-
fast_api.include_router(events.router)
31+
fast_api.include_router(api_router, prefix="/api")

python-fastapi/examples/basic-async/app/queries/create_event_async_edgeql.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def __get_validators__(cls):
3030

3131
@dataclasses.dataclass
3232
class CreateEventResult(NoPydanticValidation):
33-
host: CreateEventResultHost | None
33+
host: CreateEventResultHost
3434
schedule: datetime.datetime | None
3535
name: Str50
3636
address: str | None
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
with
2-
name := <str>$name,
2+
name := <optional str>$name,
3+
identity_id := <optional uuid>$identity_id,
4+
IDENTITY := (select ext::auth::Identity filter .id = identity_id),
35
NEW_USER := (
46
insert default::User {
5-
name := name,
7+
name := name ??
8+
assert_single(
9+
IDENTITY.<identity[is ext::auth::EmailFactor].email
10+
) ??
11+
to_str(datetime_of_statement()),
12+
identities := IDENTITY
613
}
714
),
815
select NEW_USER { * };

python-fastapi/examples/basic-async/app/queries/create_user_async_edgeql.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,27 @@ class CreateUserResult(NoPydanticValidation):
3838
async def create_user(
3939
executor: gel.AsyncIOExecutor,
4040
*,
41-
name: str,
41+
name: str | None = None,
42+
identity_id: uuid.UUID | None = None,
4243
) -> CreateUserResult:
4344
return await executor.query_single(
4445
"""\
4546
with
46-
name := <str>$name,
47+
name := <optional str>$name,
48+
identity_id := <optional uuid>$identity_id,
49+
IDENTITY := (select ext::auth::Identity filter .id = identity_id),
4750
NEW_USER := (
4851
insert default::User {
49-
name := name,
52+
name := name ??
53+
assert_single(
54+
IDENTITY.<identity[is ext::auth::EmailFactor].email
55+
) ??
56+
to_str(datetime_of_statement()),
57+
identities := IDENTITY
5058
}
5159
),
5260
select NEW_USER { * };\
5361
""",
5462
name=name,
63+
identity_id=identity_id,
5564
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
select (global current_user) { * }

0 commit comments

Comments
 (0)