Skip to content

Commit

Permalink
add fastapi example with edgedb auth (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
beerose authored Jul 25, 2024
1 parent ac414c4 commit ec5c6a8
Show file tree
Hide file tree
Showing 39 changed files with 1,062 additions and 6 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ This repo contains starter projects that demonstrate usage of EdgeDB with differ

## Python

| Framework | Link |
| ------------------ | ------------------------------------------------------------------------------------ |
| FastAPI | [fastapi-crud](https://github.com/edgedb/edgedb-examples/tree/main/fastapi-crud) |
| Flask CRUD | [flask-crud](https://github.com/edgedb/edgedb-examples/tree/main/flask-crud) |
| Flask Proxy | [flask-proxy](https://github.com/edgedb/edgedb-examples/tree/main/flask-proxy) |
| Strawberry GraphQL | [strawberry-gql](https://github.com/edgedb/edgedb-examples/tree/main/strawberry-gql) |
| Framework | Link |
| ------------------ | -------------------------------------------------------------------------------------------|
| FastAPI | [fastapi-crud](https://github.com/edgedb/edgedb-examples/tree/main/fastapi-crud) |
| FastAPI with Auth | [fastapi-crud-auth](https://github.com/edgedb/edgedb-examples/tree/main/fastapi-crud-auth) |
| Flask CRUD | [flask-crud](https://github.com/edgedb/edgedb-examples/tree/main/flask-crud) |
| Flask Proxy | [flask-proxy](https://github.com/edgedb/edgedb-examples/tree/main/flask-proxy) |
| Strawberry GraphQL | [strawberry-gql](https://github.com/edgedb/edgedb-examples/tree/main/strawberry-gql) |

## Go

Expand Down
25 changes: 25 additions & 0 deletions fastapi-crud-auth/.flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[flake8]
extend-exclude =
.git,
__pycache__,
docs/source/conf.py,
old,
build,
dist,
.venv,
venv,
myvenv,
app/queries

extend-ignore = E203, E266, E501, W605

# Black's default line length.
max-line-length = 88

max-complexity = 18

# Specify the list of error codes you wish Flake8 to report.
select = B,C,E,F,W,T4,B9

# Parallelism
jobs = 4
1 change: 1 addition & 0 deletions fastapi-crud-auth/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
myvenv
138 changes: 138 additions & 0 deletions fastapi-crud-auth/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
path := ./

define Comment
- Run `make help` to see all the available options.
- Run `make setup` to run first-time project setup.
- Run `make lint` to run the linter.
- Run `make lint-check` to check linter conformity.
endef


.PHONY: lint
lint: black isort flake mypy ## Apply all the linters.


.PHONY: lint-check
lint-check: ## Check whether the codebase satisfies the linter rules.
@echo "Checking linter rules..."
@echo "========================"
@echo
@. ./myvenv/bin/activate && black --fast --check $(path)
@. ./myvenv/bin/activate && isort --check $(path)
@. ./myvenv/bin/activate && flake8 $(path)
@. ./myvenv/bin/activate && (echo 'y' | mypy $(path) --install-types)


.PHONY: black
black: ## Apply black.
@echo
@echo "Applying black..."
@echo "================="
@echo
@. ./myvenv/bin/activate && black --fast $(path)
@echo


.PHONY: isort
isort: ## Apply isort.
@echo "Applying isort..."
@echo "================="
@echo
@. ./myvenv/bin/activate && isort $(path)


.PHONY: flake
flake: ## Apply flake8.
@echo
@echo "Applying flake8..."
@echo "================="
@echo
@. ./myvenv/bin/activate && flake8 $(path)


.PHONY: mypy
mypy: ## Apply mypy.
@echo
@echo "Applying mypy..."
@echo "================="
@echo
@. ./myvenv/bin/activate && mypy $(path)


.PHONY: help
help: ## Show this help message.
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'


.PHONY: test
test: ## Run the tests against the current version of Python.
@echo "Resetting test database..."
@edgedb query "drop database edgedb_test" > /dev/null 2>&1 || true && edgedb query "create database edgedb_test" > /dev/null 2>&1 && edgedb migrate -d edgedb_test && edgedb query -d edgedb_test -f tests/fixture.edgeql
@. ./myvenv/bin/activate && EDGEDB_DATABASE=edgedb_test pytest


.PHONY: dep-install
dep-install: ## Install latest versions of prod dependencies
@echo
@echo "Installing dependencies..."
@echo "=========================="
@. ./myvenv/bin/activate && pip install edgedb fastapi uvicorn


.PHONY: dep-install-dev
dep-install-dev: ## Install latest versions of dev dependencies
@echo
@echo "Installing dev dependencies..."
@echo "=============================="
@. ./myvenv/bin/activate && pip install 'httpx[cli]' black flake8 isort mypy pytest pytest-mock

.PHONY: install-edgedb
install-edgedb: ## Install the EdgeDB CLI
@echo
@echo "Installing EdgeDB..."
@echo "===================="
@which edgedb > /dev/null 2>&1 || (curl --proto '=https' --tlsv1.2 -sSf https://sh.edgedb.com | sh -s -- -y)


.PHONY: init-project
init-project: ## Install the EdgeDB CLI
@echo
@echo "Initializing EdgeDB project..."
@echo "=============================="
@edgedb project init --non-interactive


.PHONY: dev-server
dev-server: ## Spin up local dev server.
@. ./myvenv/bin/activate && uvicorn app.main:fast_api --port 5001 --reload


.PHONY: health-check
health-check: ## Perfrom health check on the uvicorn server.
@chmod +x ./scripts/health_check
@./scripts/health_check


.PHONY: generate
generate: ## Generate code from .edgeql files
@echo
@echo "Generating code..."
@echo "=================="
@. ./myvenv/bin/activate && edgedb-py


.PHONY: create-venv
create-venv: ## Create a virtual environment for the project
@echo "Creating virtual environment..."
@python -m venv myvenv


.PHONY: activate-venv
activate-venv: ## Activate the project's virtual environment
@echo "This cannot be done from Make. Run \`source myvenv/bin/activate\` in your shell to activate."


.PHONY: setup
setup: create-venv dep-install dep-install-dev install-edgedb init-project generate ## Run first-time setup
@echo
@echo "${Green}All set!${NC} Run \`make dev-server\` to start a uvicorn dev server on port 5001."
7 changes: 7 additions & 0 deletions fastapi-crud-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## FastAPI

Before using the app, run `make setup` to prep environment, install dependencies, and generate code. Run `make dev-server` to start a development server.

To switch to the app's virtual environment in an interactive terminal session, run `source myvenv/bin/activate`.

To learn how to build this app yourself, check out [our guide](https://www.edgedb.com/docs/guides/tutorials/rest_apis_with_fastapi).
6 changes: 6 additions & 0 deletions fastapi-crud-auth/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import edgedb
from fastapi import Request


def get_edgedb_client(request: Request) -> edgedb.AsyncIOClient:
return request.app.state.edgedb
107 changes: 107 additions & 0 deletions fastapi-crud-auth/app/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import secrets
import hashlib
import base64
import os

import edgedb
import httpx

from fastapi import APIRouter, HTTPException, Request, Cookie, Response
from fastapi.responses import JSONResponse
from pydantic import BaseModel

from .queries import create_user_async_edgeql as create_user_qry

router = APIRouter()

client = edgedb.create_async_client()

class RequestData(BaseModel):
name: str

EDGEDB_AUTH_BASE_URL = os.getenv("EDGEDB_AUTH_BASE_URL")

@router.post("/auth/signup")
async def handle_signup(request: Request):
body = await request.json()
email = body.get("email")
name = body.get("name")
password = body.get("password")

if not email or not password or not name:
raise HTTPException(status_code=400, detail="Missing email, password, or name.")

verifier, challenge = generate_pkce()
register_url = f"{EDGEDB_AUTH_BASE_URL}/register"
register_response = httpx.post(register_url, json={
"challenge": challenge,
"email": email,
"password": password,
"provider": "builtin::local_emailpassword",
"verify_url": "http://localhost:8000/auth/verify",
})

if register_response.status_code != 200 and response.status_code != 201:
return JSONResponse(status_code=400, content={"message": "Registration failed"})

code = register_response.json().get("code")
token_url = f"{EDGEDB_AUTH_BASE_URL}/token"
token_response = httpx.get(token_url, params={"code": code, "verifier": verifier})

if token_response.status_code != 200:
return JSONResponse(status_code=400, content={"message": "Token exchange failed"})

auth_token = token_response.json().get("auth_token")
identity_id = token_response.json().get("identity_id")
try:
created_user = await create_user_qry.create_user(client, name=name, identity_id=identity_id)
except edgedb.errors.ConstraintViolationError:
raise HTTPException(
status_code=400,
detail={"error": f"Username '{name}' already exists."},
)

response = JSONResponse(content={"message": "User registered"})
response.set_cookie(key="edgedb-auth-token", value=auth_token, httponly=True, secure=True, samesite='strict')
return response

@router.post("/auth/signin")
async def handle_signin(request: Request):
body = await request.json()
email = body.get("email")
password = body.get("password")
provider = body.get("provider")

if not email or not password or not provider:
raise HTTPException(status_code=400, detail="Missing email, password, or provider.")

verifier, challenge = generate_pkce()
authenticate_url = f"{EDGEDB_AUTH_BASE_URL}/authenticate"
response = httpx.post(authenticate_url, json={
"challenge": challenge,
"email": email,
"password": password,
"provider": provider,
})

if response.status_code != 200:
return JSONResponse(status_code=400, content={"message": "Authentication failed"})

code = response.json().get("code")
token_url = f"{EDGEDB_AUTH_BASE_URL}/token"
token_response = httpx.get(token_url, params={"code": code, "verifier": verifier})

if token_response.status_code != 200:
return JSONResponse(status_code=400, content={"message": "Token exchange failed"})

auth_token = token_response.json().get("auth_token")
response = JSONResponse(content={"message": "Authentication successful"})
response.set_cookie(key="edgedb-auth-token", value=auth_token, httponly=True, secure=True, samesite='strict')
return response

def generate_pkce():
verifier = secrets.token_urlsafe(32)
challenge = hashlib.sha256(verifier.encode()).digest()
challenge_base64 = base64.urlsafe_b64encode(challenge).decode('utf-8').rstrip('=')
return verifier, challenge_base64

Loading

0 comments on commit ec5c6a8

Please sign in to comment.