Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
27 changes: 26 additions & 1 deletion src/auth/Makefile
Original file line number Diff line number Diff line change
@@ -1,32 +1,57 @@
tests: pytest
help::
@echo "Available commands"
@echo " help -- (default) print this message"

tests: mypy pytest
help::
@echo " tests -- run all tests for supabase_auth"

pytest: start-infra
uv run --package supabase_auth pytest --cov=./ --cov-report=xml --cov-report=html -vv

mypy:
uv run --package supabase_auth mypy src/supabase_auth tests
help::
@echo " mypy -- run mypy on supabase_auth"

start-infra:
cd infra &&\
docker compose down &&\
docker compose up -d
sleep 2
help::
@echo " start-infra -- start containers for tests"

clean-infra:
cd infra &&\
docker compose down --remove-orphans &&\
docker system prune -a --volumes -f
help::
@echo " clean-infra -- delete all stored information about the containers"

stop-infra:
cd infra &&\
docker compose down --remove-orphans
help::
@echo " stop-infra -- stop containers for tests"

sync-infra:
uv run --package supabase_auth scripts/gh-download.py --repo=supabase/gotrue-js --branch=master --folder=infra
help::
@echo " sync-infra -- update locked versions for test containers"

build-sync:
uv run --package supabase_auth scripts/run-unasync.py
help::
@echo " build-sync -- generate _sync from _async code"

clean:
rm -rf htmlcov .pytest_cache .mypy_cache .ruff_cache
rm -f .coverage coverage.xml
help::
@echo " clean -- clean intermediary files"

build:
uv build --package supabase_auth
help::
@echo " build -- invoke uv build on supabase_auth package"
4 changes: 4 additions & 0 deletions src/auth/infra/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ services:
GOTRUE_LOG_LEVEL: DEBUG
GOTRUE_OPERATOR_TOKEN: super-secret-operator-token
DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable'
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: 'true'
GOTRUE_EXTERNAL_GOOGLE_ENABLED: 'true'
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: 53566906701-bmhc1ndue7hild39575gkpimhs06b7ds.apps.googleusercontent.com
GOTRUE_EXTERNAL_GOOGLE_SECRET: Sm3s8RE85rDcS36iMy8YjrpC
Expand Down Expand Up @@ -61,6 +62,7 @@ services:
GOTRUE_LOG_LEVEL: DEBUG
GOTRUE_OPERATOR_TOKEN: super-secret-operator-token
DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable'
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: 'true'
GOTRUE_EXTERNAL_PHONE_ENABLED: 'true'
GOTRUE_SMTP_HOST: mail
GOTRUE_SMTP_PORT: 2500
Expand Down Expand Up @@ -90,6 +92,7 @@ services:
GOTRUE_SMS_AUTOCONFIRM: 'true'
GOTRUE_LOG_LEVEL: DEBUG
GOTRUE_OPERATOR_TOKEN: super-secret-operator-token
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: 'true'
DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable'
GOTRUE_EXTERNAL_PHONE_ENABLED: 'true'
GOTRUE_SMTP_HOST: mail
Expand Down Expand Up @@ -119,6 +122,7 @@ services:
GOTRUE_LOG_LEVEL: DEBUG
GOTRUE_OPERATOR_TOKEN: super-secret-operator-token
DATABASE_URL: 'postgres://postgres:postgres@db:5432/postgres?sslmode=disable'
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: 'true'
GOTRUE_EXTERNAL_PHONE_ENABLED: 'false'
GOTRUE_EXTERNAL_EMAIL_ENABLED: 'false'
GOTRUE_SMTP_HOST: mail
Expand Down
15 changes: 15 additions & 0 deletions src/auth/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ tests = [
lints = [
"ruff >=0.12.1",
"unasync >= 0.6.0",
"python-lsp-server (>=1.12.2,<2.0.0)",
"pylsp-mypy (>=0.7.0,<0.8.0)",
"python-lsp-ruff (>=2.2.2,<3.0.0)",
]
dev = [{ include-group = "lints" }, {include-group = "tests" }]

Expand Down Expand Up @@ -76,3 +79,15 @@ asyncio_mode = "auto"
[build-system]
requires = ["uv_build>=0.8.3,<0.9.0"]
build-backend = "uv_build"

[tool.mypy]
python_version = "3.9"
check_untyped_defs = true
allow_redefinition = true
follow_untyped_imports = true # for deprecation module that does not have stubs

no_warn_no_return = true
warn_return_any = true
warn_unused_configs = true
warn_redundant_casts = true
warn_unused_ignores = true
2 changes: 1 addition & 1 deletion src/auth/scripts/run-unasync.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import unasync

paths = Path("../src/supabase").glob("**/*.py")
paths = Path("src/supabase_auth").glob("**/*.py")
tests = Path("tests").glob("**/*.py")

rules = (unasync._DEFAULT_RULE,)
Expand Down
18 changes: 9 additions & 9 deletions src/auth/src/supabase_auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
from __future__ import annotations

from ._async.gotrue_admin_api import AsyncGoTrueAdminAPI # type: ignore # noqa: F401
from ._async.gotrue_client import AsyncGoTrueClient # type: ignore # noqa: F401
from ._async.gotrue_admin_api import AsyncGoTrueAdminAPI
from ._async.gotrue_client import AsyncGoTrueClient
from ._async.storage import (
AsyncMemoryStorage, # type: ignore # noqa: F401
AsyncSupportedStorage, # type: ignore # noqa: F401
AsyncMemoryStorage,
AsyncSupportedStorage,
)
from ._sync.gotrue_admin_api import SyncGoTrueAdminAPI # type: ignore # noqa: F401
from ._sync.gotrue_client import SyncGoTrueClient # type: ignore # noqa: F401
from ._sync.gotrue_admin_api import SyncGoTrueAdminAPI
from ._sync.gotrue_client import SyncGoTrueClient
from ._sync.storage import (
SyncMemoryStorage, # type: ignore # noqa: F401
SyncSupportedStorage, # type: ignore # noqa: F401
SyncMemoryStorage,
SyncSupportedStorage,
)
from .types import * # type: ignore # noqa: F401, F403
from .types import *
from .version import __version__
65 changes: 35 additions & 30 deletions src/auth/src/supabase_auth/_async/gotrue_admin_api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations

from functools import partial
from typing import Dict, List, Optional
from typing import Any, Dict, List, Optional

from httpx import QueryParams, Response
from pydantic import TypeAdapter

from ..helpers import (
is_valid_uuid,
Expand All @@ -21,6 +23,7 @@
InviteUserByEmailOptions,
SignOutScope,
User,
UserList,
UserResponse,
)
from .gotrue_admin_mfa_api import AsyncGoTrueAdminMFAAPI
Expand All @@ -45,18 +48,19 @@ def __init__(
verify=verify,
proxy=proxy,
)
# TODO(@o-santi): why is is this done this way?
self.mfa = AsyncGoTrueAdminMFAAPI()
self.mfa.list_factors = self._list_factors
self.mfa.delete_factor = self._delete_factor
self.mfa.list_factors = self._list_factors # type: ignore
self.mfa.delete_factor = self._delete_factor # type: ignore

async def sign_out(self, jwt: str, scope: SignOutScope = "global") -> None:
"""
Removes a logged-in session.
"""
return await self._request(
await self._request(
"POST",
"logout",
query={"scope": scope},
query=QueryParams(scope=scope),
jwt=jwt,
no_resolve_json=True,
)
Expand All @@ -69,19 +73,19 @@ async def invite_user_by_email(
"""
Sends an invite link to an email address.
"""
return await self._request(
response = await self._request(
"POST",
"invite",
body={"email": email, "data": options.get("data")},
redirect_to=options.get("redirect_to"),
xform=parse_user_response,
)
return parse_user_response(response)

async def generate_link(self, params: GenerateLinkParams) -> GenerateLinkResponse:
"""
Generates email links and OTPs to be sent via a custom email provider.
"""
return await self._request(
response = await self._request(
"POST",
"admin/generate_link",
body={
Expand All @@ -92,9 +96,10 @@ async def generate_link(self, params: GenerateLinkParams) -> GenerateLinkRespons
"data": params.get("options", {}).get("data"),
},
redirect_to=params.get("options", {}).get("redirect_to"),
xform=parse_link_response,
)

return parse_link_response(response)

# User Admin API

async def create_user(self, attributes: AdminUserAttributes) -> UserResponse:
Expand All @@ -104,30 +109,28 @@ async def create_user(self, attributes: AdminUserAttributes) -> UserResponse:
This function should only be called on a server.
Never expose your `service_role` key in the browser.
"""
return await self._request(
response = await self._request(
"POST",
"admin/users",
body=attributes,
xform=parse_user_response,
)
return parse_user_response(response)

async def list_users(self, page: int = None, per_page: int = None) -> List[User]:
async def list_users(
self, page: Optional[int] = None, per_page: Optional[int] = None
) -> List[User]:
"""
Get a list of users.

This function should only be called on a server.
Never expose your `service_role` key in the browser.
"""
return await self._request(
response = await self._request(
"GET",
"admin/users",
query={"page": page, "per_page": per_page},
xform=lambda data: (
[model_validate(User, user) for user in data["users"]]
if "users" in data
else []
),
query=QueryParams(page=page, per_page=per_page),
)
return model_validate(UserList, response.content).users

async def get_user_by_id(self, uid: str) -> UserResponse:
"""
Expand All @@ -138,11 +141,11 @@ async def get_user_by_id(self, uid: str) -> UserResponse:
"""
self._validate_uuid(uid)

return await self._request(
response = await self._request(
"GET",
f"admin/users/{uid}",
xform=parse_user_response,
)
return parse_user_response(response)

async def update_user_by_id(
self,
Expand All @@ -156,12 +159,12 @@ async def update_user_by_id(
Never expose your `service_role` key in the browser.
"""
self._validate_uuid(uid)
return await self._request(
response = await self._request(
"PUT",
f"admin/users/{uid}",
body=attributes,
xform=parse_user_response,
)
return parse_user_response(response)

async def delete_user(self, id: str, should_soft_delete: bool = False) -> None:
"""
Expand All @@ -172,31 +175,33 @@ async def delete_user(self, id: str, should_soft_delete: bool = False) -> None:
"""
self._validate_uuid(id)
body = {"should_soft_delete": should_soft_delete}
return await self._request("DELETE", f"admin/users/{id}", body=body)
await self._request("DELETE", f"admin/users/{id}", body=body)

async def _list_factors(
self,
params: AuthMFAAdminListFactorsParams,
) -> AuthMFAAdminListFactorsResponse:
self._validate_uuid(params.get("user_id"))
return await self._request(
response = await self._request(
"GET",
f"admin/users/{params.get('user_id')}/factors",
xform=partial(model_validate, AuthMFAAdminListFactorsResponse),
)
return model_validate(AuthMFAAdminListFactorsResponse, response.content)

async def _delete_factor(
self,
params: AuthMFAAdminDeleteFactorParams,
) -> AuthMFAAdminDeleteFactorResponse:
self._validate_uuid(params.get("user_id"))
self._validate_uuid(params.get("id"))
return await self._request(
response = await self._request(
"DELETE",
f"admin/users/{params.get('user_id')}/factors/{params.get('id')}",
xform=partial(model_validate, AuthMFAAdminDeleteFactorResponse),
)
return model_validate(AuthMFAAdminDeleteFactorResponse, response.content)

def _validate_uuid(self, id: str) -> None:
def _validate_uuid(self, id: str | None) -> None:
if id is None:
raise ValueError("Invalid id, id cannot be none")
if not is_valid_uuid(id):
raise ValueError(f"Invalid id, '{id}' is not a valid uuid")
Loading