- Remove env-based site discovery from SiteManager and ProjectManager - Unify admin and user OAuth Clients into single role-based page - Update all documentation to reflect DB-based site management - Clean up env.example, README, ARCHITECTURE docs - Improve Projects page empty state with link to My Sites - Remove obsolete env discovery tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
541 lines
19 KiB
Python
541 lines
19 KiB
Python
"""Auth Handler - manages Supabase authentication via GoTrue Admin API"""
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from plugins.supabase.client import SupabaseClient
|
|
|
|
|
|
def get_tool_specifications() -> list[dict[str, Any]]:
|
|
"""Return tool specifications for ToolGenerator (14 tools)"""
|
|
return [
|
|
{
|
|
"name": "list_users",
|
|
"method_name": "list_users",
|
|
"description": "List all users with pagination. Returns user details including email, phone, metadata, and confirmation status.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"page": {
|
|
"type": "integer",
|
|
"description": "Page number (1-indexed)",
|
|
"default": 1,
|
|
},
|
|
"per_page": {
|
|
"type": "integer",
|
|
"description": "Users per page (max 1000)",
|
|
"default": 50,
|
|
},
|
|
},
|
|
},
|
|
"scope": "admin",
|
|
},
|
|
{
|
|
"name": "get_user",
|
|
"method_name": "get_user",
|
|
"description": "Get detailed information about a specific user by their ID.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {"user_id": {"type": "string", "description": "User UUID"}},
|
|
"required": ["user_id"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "create_user",
|
|
"method_name": "create_user",
|
|
"description": "Create a new user with email and password. Can optionally auto-confirm email and set metadata.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"email": {
|
|
"type": "string",
|
|
"format": "email",
|
|
"description": "User email address",
|
|
},
|
|
"password": {
|
|
"type": "string",
|
|
"minLength": 6,
|
|
"description": "Password (min 6 characters)",
|
|
},
|
|
"phone": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "Phone number in E.164 format",
|
|
},
|
|
"email_confirm": {
|
|
"type": "boolean",
|
|
"description": "Auto-confirm email without verification",
|
|
"default": False,
|
|
},
|
|
"user_metadata": {
|
|
"anyOf": [{"type": "object"}, {"type": "null"}],
|
|
"description": "Custom user metadata (name, avatar, etc.)",
|
|
},
|
|
"app_metadata": {
|
|
"anyOf": [{"type": "object"}, {"type": "null"}],
|
|
"description": "App-specific metadata (roles, permissions)",
|
|
},
|
|
},
|
|
"required": ["email", "password"],
|
|
},
|
|
"scope": "admin",
|
|
},
|
|
{
|
|
"name": "update_user",
|
|
"method_name": "update_user",
|
|
"description": "Update user details including email, password, phone, metadata, or ban status.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"user_id": {"type": "string", "description": "User UUID"},
|
|
"email": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "New email address",
|
|
},
|
|
"password": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "New password",
|
|
},
|
|
"phone": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "New phone number",
|
|
},
|
|
"email_confirm": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"description": "Set email confirmation status",
|
|
},
|
|
"phone_confirm": {
|
|
"anyOf": [{"type": "boolean"}, {"type": "null"}],
|
|
"description": "Set phone confirmation status",
|
|
},
|
|
"user_metadata": {
|
|
"anyOf": [{"type": "object"}, {"type": "null"}],
|
|
"description": "Update user metadata",
|
|
},
|
|
"app_metadata": {
|
|
"anyOf": [{"type": "object"}, {"type": "null"}],
|
|
"description": "Update app metadata",
|
|
},
|
|
},
|
|
"required": ["user_id"],
|
|
},
|
|
"scope": "admin",
|
|
},
|
|
{
|
|
"name": "delete_user",
|
|
"method_name": "delete_user",
|
|
"description": "Permanently delete a user and all their data.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {"user_id": {"type": "string", "description": "User UUID to delete"}},
|
|
"required": ["user_id"],
|
|
},
|
|
"scope": "admin",
|
|
},
|
|
{
|
|
"name": "invite_user",
|
|
"method_name": "invite_user",
|
|
"description": "Send an email invitation to a new user. Creates user in 'invited' state.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"email": {
|
|
"type": "string",
|
|
"format": "email",
|
|
"description": "Email to invite",
|
|
},
|
|
"redirect_to": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "URL to redirect after accepting invitation",
|
|
},
|
|
"user_metadata": {
|
|
"anyOf": [{"type": "object"}, {"type": "null"}],
|
|
"description": "Initial user metadata",
|
|
},
|
|
},
|
|
"required": ["email"],
|
|
},
|
|
"scope": "admin",
|
|
},
|
|
{
|
|
"name": "generate_link",
|
|
"method_name": "generate_link",
|
|
"description": "Generate a magic link, recovery link, or invite link for a user.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"email": {"type": "string", "format": "email", "description": "User email"},
|
|
"link_type": {
|
|
"type": "string",
|
|
"enum": [
|
|
"magiclink",
|
|
"recovery",
|
|
"invite",
|
|
"signup",
|
|
"email_change_new",
|
|
"email_change_current",
|
|
],
|
|
"description": "Type of link to generate",
|
|
"default": "magiclink",
|
|
},
|
|
"redirect_to": {
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"description": "URL to redirect after link is used",
|
|
},
|
|
},
|
|
"required": ["email"],
|
|
},
|
|
"scope": "admin",
|
|
},
|
|
{
|
|
"name": "ban_user",
|
|
"method_name": "ban_user",
|
|
"description": "Ban a user for a specified duration. Banned users cannot sign in.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"user_id": {"type": "string", "description": "User UUID"},
|
|
"duration": {
|
|
"type": "string",
|
|
"description": "Ban duration (e.g., '24h', '7d', '1y', 'none' for permanent)",
|
|
"default": "none",
|
|
},
|
|
},
|
|
"required": ["user_id"],
|
|
},
|
|
"scope": "admin",
|
|
},
|
|
{
|
|
"name": "unban_user",
|
|
"method_name": "unban_user",
|
|
"description": "Remove ban from a user, allowing them to sign in again.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {"user_id": {"type": "string", "description": "User UUID"}},
|
|
"required": ["user_id"],
|
|
},
|
|
"scope": "admin",
|
|
},
|
|
{
|
|
"name": "list_user_factors",
|
|
"method_name": "list_user_factors",
|
|
"description": "List all MFA factors (TOTP, phone) for a user.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {"user_id": {"type": "string", "description": "User UUID"}},
|
|
"required": ["user_id"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "delete_user_factor",
|
|
"method_name": "delete_user_factor",
|
|
"description": "Delete an MFA factor from a user.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"user_id": {"type": "string", "description": "User UUID"},
|
|
"factor_id": {"type": "string", "description": "Factor UUID to delete"},
|
|
},
|
|
"required": ["user_id", "factor_id"],
|
|
},
|
|
"scope": "admin",
|
|
},
|
|
{
|
|
"name": "search_users",
|
|
"method_name": "search_users",
|
|
"description": "Search users by email or phone number.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {"type": "string", "description": "Search query (email or phone)"},
|
|
"page": {"type": "integer", "default": 1},
|
|
"per_page": {"type": "integer", "default": 50},
|
|
},
|
|
"required": ["query"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
{
|
|
"name": "get_user_by_email",
|
|
"method_name": "get_user_by_email",
|
|
"description": "Find a user by their email address.",
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"email": {
|
|
"type": "string",
|
|
"format": "email",
|
|
"description": "Email to search for",
|
|
}
|
|
},
|
|
"required": ["email"],
|
|
},
|
|
"scope": "read",
|
|
},
|
|
]
|
|
|
|
|
|
# =====================
|
|
# Auth Operations (14 tools)
|
|
# =====================
|
|
|
|
|
|
async def list_users(client: SupabaseClient, page: int = 1, per_page: int = 50) -> str:
|
|
"""List all users with pagination"""
|
|
try:
|
|
result = await client.list_users(page=page, per_page=per_page)
|
|
|
|
users = result.get("users", []) if isinstance(result, dict) else result
|
|
result.get("aud", len(users)) if isinstance(result, dict) else len(users)
|
|
|
|
return json.dumps(
|
|
{
|
|
"success": True,
|
|
"page": page,
|
|
"per_page": per_page,
|
|
"count": len(users) if isinstance(users, list) else 0,
|
|
"users": users,
|
|
},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
|
|
|
|
async def get_user(client: SupabaseClient, user_id: str) -> str:
|
|
"""Get user by ID"""
|
|
try:
|
|
result = await client.get_user(user_id)
|
|
|
|
return json.dumps({"success": True, "user": result}, indent=2, ensure_ascii=False)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
|
|
|
|
async def create_user(
|
|
client: SupabaseClient,
|
|
email: str,
|
|
password: str,
|
|
phone: str | None = None,
|
|
email_confirm: bool = False,
|
|
user_metadata: dict | None = None,
|
|
app_metadata: dict | None = None,
|
|
) -> str:
|
|
"""Create a new user"""
|
|
try:
|
|
result = await client.create_user(
|
|
email=email,
|
|
password=password,
|
|
phone=phone,
|
|
email_confirm=email_confirm,
|
|
user_metadata=user_metadata,
|
|
app_metadata=app_metadata,
|
|
)
|
|
|
|
return json.dumps(
|
|
{"success": True, "message": "User created successfully", "user": result},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
|
|
|
|
async def update_user(
|
|
client: SupabaseClient,
|
|
user_id: str,
|
|
email: str | None = None,
|
|
password: str | None = None,
|
|
phone: str | None = None,
|
|
email_confirm: bool | None = None,
|
|
phone_confirm: bool | None = None,
|
|
user_metadata: dict | None = None,
|
|
app_metadata: dict | None = None,
|
|
) -> str:
|
|
"""Update user details"""
|
|
try:
|
|
result = await client.update_user(
|
|
user_id=user_id,
|
|
email=email,
|
|
password=password,
|
|
phone=phone,
|
|
email_confirm=email_confirm,
|
|
phone_confirm=phone_confirm,
|
|
user_metadata=user_metadata,
|
|
app_metadata=app_metadata,
|
|
)
|
|
|
|
return json.dumps(
|
|
{"success": True, "message": "User updated successfully", "user": result},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
|
|
|
|
async def delete_user(client: SupabaseClient, user_id: str) -> str:
|
|
"""Delete a user"""
|
|
try:
|
|
await client.delete_user(user_id)
|
|
|
|
return json.dumps(
|
|
{"success": True, "message": f"User {user_id} deleted successfully"},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
|
|
|
|
async def invite_user(
|
|
client: SupabaseClient,
|
|
email: str,
|
|
redirect_to: str | None = None,
|
|
user_metadata: dict | None = None,
|
|
) -> str:
|
|
"""Invite a user by email"""
|
|
try:
|
|
result = await client.generate_link(
|
|
email=email, link_type="invite", redirect_to=redirect_to
|
|
)
|
|
|
|
return json.dumps(
|
|
{"success": True, "message": f"Invitation sent to {email}", "result": result},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
|
|
|
|
async def generate_link(
|
|
client: SupabaseClient, email: str, link_type: str = "magiclink", redirect_to: str | None = None
|
|
) -> str:
|
|
"""Generate auth link (magic link, recovery, invite)"""
|
|
try:
|
|
result = await client.generate_link(
|
|
email=email, link_type=link_type, redirect_to=redirect_to
|
|
)
|
|
|
|
return json.dumps(
|
|
{"success": True, "link_type": link_type, "email": email, "result": result},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
|
|
|
|
async def ban_user(client: SupabaseClient, user_id: str, duration: str = "none") -> str:
|
|
"""Ban a user"""
|
|
try:
|
|
result = await client.update_user(user_id=user_id, ban_duration=duration)
|
|
|
|
return json.dumps(
|
|
{"success": True, "message": f"User {user_id} banned for {duration}", "user": result},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
|
|
|
|
async def unban_user(client: SupabaseClient, user_id: str) -> str:
|
|
"""Unban a user"""
|
|
try:
|
|
result = await client.update_user(user_id=user_id, ban_duration="0")
|
|
|
|
return json.dumps(
|
|
{"success": True, "message": f"User {user_id} unbanned", "user": result},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
|
|
|
|
async def list_user_factors(client: SupabaseClient, user_id: str) -> str:
|
|
"""List user MFA factors"""
|
|
try:
|
|
result = await client.list_user_factors(user_id)
|
|
|
|
return json.dumps(
|
|
{"success": True, "user_id": user_id, "factors": result}, indent=2, ensure_ascii=False
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
|
|
|
|
async def delete_user_factor(client: SupabaseClient, user_id: str, factor_id: str) -> str:
|
|
"""Delete an MFA factor"""
|
|
try:
|
|
await client.delete_user_factor(user_id, factor_id)
|
|
|
|
return json.dumps(
|
|
{"success": True, "message": f"Factor {factor_id} deleted from user {user_id}"},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
|
|
|
|
|
|
|
|
async def search_users(
|
|
client: SupabaseClient, query: str, page: int = 1, per_page: int = 50
|
|
) -> str:
|
|
"""Search users by email or phone"""
|
|
try:
|
|
# Get all users and filter
|
|
result = await client.list_users(page=page, per_page=per_page)
|
|
|
|
users = result.get("users", []) if isinstance(result, dict) else result
|
|
|
|
# Filter by query
|
|
query_lower = query.lower()
|
|
filtered = [
|
|
u
|
|
for u in users
|
|
if (
|
|
u.get("email", "").lower().find(query_lower) >= 0
|
|
or u.get("phone", "").find(query) >= 0
|
|
)
|
|
]
|
|
|
|
return json.dumps(
|
|
{"success": True, "query": query, "count": len(filtered), "users": filtered},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|
|
|
|
|
|
async def get_user_by_email(client: SupabaseClient, email: str) -> str:
|
|
"""Find user by email"""
|
|
try:
|
|
# Get all users and find by email
|
|
result = await client.list_users(page=1, per_page=1000)
|
|
|
|
users = result.get("users", []) if isinstance(result, dict) else result
|
|
|
|
# Find exact match
|
|
user = next((u for u in users if u.get("email", "").lower() == email.lower()), None)
|
|
|
|
if user:
|
|
return json.dumps(
|
|
{"success": True, "found": True, "user": user}, indent=2, ensure_ascii=False
|
|
)
|
|
else:
|
|
return json.dumps(
|
|
{"success": True, "found": False, "message": f"No user found with email: {email}"},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
)
|
|
except Exception as e:
|
|
return json.dumps({"success": False, "error": str(e)}, indent=2, ensure_ascii=False)
|