-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add fastapi example with edgedb auth (#135)
- Loading branch information
Showing
39 changed files
with
1,062 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
myvenv |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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." |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
Oops, something went wrong.