Yassine Mhirsi
commited on
Commit
·
6e8d513
1
Parent(s):
2ed0ab3
feat: Implement user management service with Supabase integration, including user registration, retrieval, and name update endpoints.
Browse files- config.py +5 -0
- models/__init__.py +13 -0
- models/user.py +80 -0
- requirements.txt +1 -0
- routes/__init__.py +2 -1
- routes/user.py +128 -0
- services/__init__.py +6 -0
- services/database_service.py +48 -0
- services/user_service.py +167 -0
config.py
CHANGED
|
@@ -39,6 +39,10 @@ GROQ_TTS_FORMAT = "wav"
|
|
| 39 |
# **Chat Model**
|
| 40 |
GROQ_CHAT_MODEL = "llama3-70b-8192"
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
# ============ API META ============
|
| 43 |
API_TITLE = "NLP Debater - Voice Chatbot"
|
| 44 |
API_DESCRIPTION = "NLP stance detection, KPA, and Groq STT/TTS chatbot"
|
|
@@ -80,4 +84,5 @@ logger.info(f" HF Label Model : {HUGGINGFACE_LABEL_MODEL_ID}")
|
|
| 80 |
logger.info(f" GROQ STT Model : {GROQ_STT_MODEL}")
|
| 81 |
logger.info(f" GROQ TTS Model : {GROQ_TTS_MODEL}")
|
| 82 |
logger.info(f" GROQ Chat Model : {GROQ_CHAT_MODEL}")
|
|
|
|
| 83 |
logger.info("="*60)
|
|
|
|
| 39 |
# **Chat Model**
|
| 40 |
GROQ_CHAT_MODEL = "llama3-70b-8192"
|
| 41 |
|
| 42 |
+
# ============ SUPABASE ============
|
| 43 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL", "")
|
| 44 |
+
SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
|
| 45 |
+
|
| 46 |
# ============ API META ============
|
| 47 |
API_TITLE = "NLP Debater - Voice Chatbot"
|
| 48 |
API_DESCRIPTION = "NLP stance detection, KPA, and Groq STT/TTS chatbot"
|
|
|
|
| 84 |
logger.info(f" GROQ STT Model : {GROQ_STT_MODEL}")
|
| 85 |
logger.info(f" GROQ TTS Model : {GROQ_TTS_MODEL}")
|
| 86 |
logger.info(f" GROQ Chat Model : {GROQ_CHAT_MODEL}")
|
| 87 |
+
logger.info(f" Supabase URL : {'✓ Configured' if SUPABASE_URL else '✗ Not configured'}")
|
| 88 |
logger.info("="*60)
|
models/__init__.py
CHANGED
|
@@ -36,6 +36,14 @@ from .topic import (
|
|
| 36 |
BatchTopicResponse,
|
| 37 |
)
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
# Import MCP-related schemas
|
| 40 |
from .mcp_models import (
|
| 41 |
ToolCallRequest,
|
|
@@ -72,6 +80,11 @@ __all__ = [
|
|
| 72 |
"TopicResponse",
|
| 73 |
"BatchTopicRequest",
|
| 74 |
"BatchTopicResponse",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
# MCP schemas
|
| 76 |
"ToolCallRequest",
|
| 77 |
"ToolCallResponse",
|
|
|
|
| 36 |
BatchTopicResponse,
|
| 37 |
)
|
| 38 |
|
| 39 |
+
# Import user-related schemas
|
| 40 |
+
from .user import (
|
| 41 |
+
UserRegisterRequest,
|
| 42 |
+
UserResponse,
|
| 43 |
+
UserUpdateNameRequest,
|
| 44 |
+
UserGetRequest,
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
# Import MCP-related schemas
|
| 48 |
from .mcp_models import (
|
| 49 |
ToolCallRequest,
|
|
|
|
| 80 |
"TopicResponse",
|
| 81 |
"BatchTopicRequest",
|
| 82 |
"BatchTopicResponse",
|
| 83 |
+
# User schemas
|
| 84 |
+
"UserRegisterRequest",
|
| 85 |
+
"UserResponse",
|
| 86 |
+
"UserUpdateNameRequest",
|
| 87 |
+
"UserGetRequest",
|
| 88 |
# MCP schemas
|
| 89 |
"ToolCallRequest",
|
| 90 |
"ToolCallResponse",
|
models/user.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pydantic models for user endpoints"""
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel, Field, ConfigDict
|
| 4 |
+
from typing import Optional
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class UserRegisterRequest(BaseModel):
|
| 9 |
+
"""Request model for user registration"""
|
| 10 |
+
model_config = ConfigDict(
|
| 11 |
+
json_schema_extra={
|
| 12 |
+
"example": {
|
| 13 |
+
"unique_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 14 |
+
"name": "John Doe"
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
unique_id: str = Field(
|
| 20 |
+
..., min_length=1, max_length=255,
|
| 21 |
+
description="Browser-generated unique identifier (stored in localStorage)"
|
| 22 |
+
)
|
| 23 |
+
name: Optional[str] = Field(
|
| 24 |
+
None, min_length=1, max_length=100,
|
| 25 |
+
description="User's display name (optional, will generate random if not provided)"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class UserResponse(BaseModel):
|
| 30 |
+
"""Response model for user data"""
|
| 31 |
+
model_config = ConfigDict(
|
| 32 |
+
json_schema_extra={
|
| 33 |
+
"example": {
|
| 34 |
+
"id": "123e4567-e89b-12d3-a456-426614174000",
|
| 35 |
+
"unique_id": "550e8400-e29b-41d4-a716-446655440000",
|
| 36 |
+
"name": "John Doe",
|
| 37 |
+
"created_at": "2024-01-01T12:00:00Z",
|
| 38 |
+
"updated_at": "2024-01-01T12:00:00Z"
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
id: str = Field(..., description="User UUID")
|
| 44 |
+
unique_id: str = Field(..., description="Browser-generated unique identifier")
|
| 45 |
+
name: str = Field(..., description="User's display name")
|
| 46 |
+
created_at: str = Field(..., description="User creation timestamp")
|
| 47 |
+
updated_at: str = Field(..., description="User last update timestamp")
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class UserUpdateNameRequest(BaseModel):
|
| 51 |
+
"""Request model for updating user name"""
|
| 52 |
+
model_config = ConfigDict(
|
| 53 |
+
json_schema_extra={
|
| 54 |
+
"example": {
|
| 55 |
+
"name": "Jane Doe"
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
name: str = Field(
|
| 61 |
+
..., min_length=1, max_length=100,
|
| 62 |
+
description="New display name"
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class UserGetRequest(BaseModel):
|
| 67 |
+
"""Request model for getting user by unique_id"""
|
| 68 |
+
model_config = ConfigDict(
|
| 69 |
+
json_schema_extra={
|
| 70 |
+
"example": {
|
| 71 |
+
"unique_id": "550e8400-e29b-41d4-a716-446655440000"
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
unique_id: str = Field(
|
| 77 |
+
..., min_length=1, max_length=255,
|
| 78 |
+
description="Browser-generated unique identifier"
|
| 79 |
+
)
|
| 80 |
+
|
requirements.txt
CHANGED
|
@@ -7,6 +7,7 @@ pydantic>=2.5.0
|
|
| 7 |
# API Clients
|
| 8 |
requests>=2.31.0
|
| 9 |
groq>=0.9.0
|
|
|
|
| 10 |
|
| 11 |
# LangChain
|
| 12 |
langchain>=0.1.0
|
|
|
|
| 7 |
# API Clients
|
| 8 |
requests>=2.31.0
|
| 9 |
groq>=0.9.0
|
| 10 |
+
supabase>=2.0.0
|
| 11 |
|
| 12 |
# LangChain
|
| 13 |
langchain>=0.1.0
|
routes/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""API route handlers"""
|
| 2 |
|
| 3 |
from fastapi import APIRouter
|
| 4 |
-
from . import root, health, stance, label, generate, topic
|
| 5 |
from routes.tts_routes import router as audio_router
|
| 6 |
# Create main router
|
| 7 |
api_router = APIRouter()
|
|
@@ -13,6 +13,7 @@ api_router.include_router(stance.router, prefix="/stance")
|
|
| 13 |
api_router.include_router(label.router, prefix="/label")
|
| 14 |
api_router.include_router(generate.router, prefix="/generate")
|
| 15 |
api_router.include_router(topic.router, prefix="/topic")
|
|
|
|
| 16 |
api_router.include_router(audio_router)
|
| 17 |
|
| 18 |
__all__ = ["api_router"]
|
|
|
|
| 1 |
"""API route handlers"""
|
| 2 |
|
| 3 |
from fastapi import APIRouter
|
| 4 |
+
from . import root, health, stance, label, generate, topic, user
|
| 5 |
from routes.tts_routes import router as audio_router
|
| 6 |
# Create main router
|
| 7 |
api_router = APIRouter()
|
|
|
|
| 13 |
api_router.include_router(label.router, prefix="/label")
|
| 14 |
api_router.include_router(generate.router, prefix="/generate")
|
| 15 |
api_router.include_router(topic.router, prefix="/topic")
|
| 16 |
+
api_router.include_router(user.router, prefix="/user")
|
| 17 |
api_router.include_router(audio_router)
|
| 18 |
|
| 19 |
__all__ = ["api_router"]
|
routes/user.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""User management endpoints"""
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, HTTPException, Header
|
| 4 |
+
from typing import Optional
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
from services.user_service import user_service
|
| 8 |
+
from models.user import (
|
| 9 |
+
UserRegisterRequest,
|
| 10 |
+
UserResponse,
|
| 11 |
+
UserUpdateNameRequest,
|
| 12 |
+
UserGetRequest,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
router = APIRouter()
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@router.post("/register", response_model=UserResponse, tags=["Users"])
|
| 20 |
+
async def register_user(request: UserRegisterRequest):
|
| 21 |
+
"""
|
| 22 |
+
Register a new user or get existing user by unique_id
|
| 23 |
+
|
| 24 |
+
- **unique_id**: Browser-generated unique identifier (from localStorage)
|
| 25 |
+
- **name**: Optional display name (will generate random if not provided)
|
| 26 |
+
|
| 27 |
+
Returns user data including the user_id to store in localStorage
|
| 28 |
+
"""
|
| 29 |
+
try:
|
| 30 |
+
user = user_service.register_or_get_user(
|
| 31 |
+
unique_id=request.unique_id,
|
| 32 |
+
name=request.name
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
logger.info(f"User registered/retrieved: {user.get('id')}")
|
| 36 |
+
return UserResponse(**user)
|
| 37 |
+
|
| 38 |
+
except ValueError as e:
|
| 39 |
+
logger.error(f"Validation error: {str(e)}")
|
| 40 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 41 |
+
except Exception as e:
|
| 42 |
+
logger.error(f"Registration error: {str(e)}")
|
| 43 |
+
raise HTTPException(status_code=500, detail=f"Registration failed: {str(e)}")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
@router.get("/me", response_model=UserResponse, tags=["Users"])
|
| 47 |
+
async def get_current_user(x_user_id: Optional[str] = Header(None, alias="X-User-ID")):
|
| 48 |
+
"""
|
| 49 |
+
Get current user by user_id from header
|
| 50 |
+
|
| 51 |
+
- **X-User-ID**: User UUID (sent in request header)
|
| 52 |
+
|
| 53 |
+
Returns user data
|
| 54 |
+
"""
|
| 55 |
+
if not x_user_id:
|
| 56 |
+
raise HTTPException(status_code=400, detail="X-User-ID header is required")
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
user = user_service.get_user_by_id(x_user_id)
|
| 60 |
+
|
| 61 |
+
if not user:
|
| 62 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 63 |
+
|
| 64 |
+
return UserResponse(**user)
|
| 65 |
+
|
| 66 |
+
except HTTPException:
|
| 67 |
+
raise
|
| 68 |
+
except Exception as e:
|
| 69 |
+
logger.error(f"Error getting user: {str(e)}")
|
| 70 |
+
raise HTTPException(status_code=500, detail=f"Failed to get user: {str(e)}")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@router.get("/by-unique-id", response_model=UserResponse, tags=["Users"])
|
| 74 |
+
async def get_user_by_unique_id(unique_id: str):
|
| 75 |
+
"""
|
| 76 |
+
Get user by unique_id (query parameter)
|
| 77 |
+
|
| 78 |
+
- **unique_id**: Browser-generated unique identifier
|
| 79 |
+
|
| 80 |
+
Returns user data
|
| 81 |
+
"""
|
| 82 |
+
try:
|
| 83 |
+
user = user_service.get_user_by_unique_id(unique_id)
|
| 84 |
+
|
| 85 |
+
if not user:
|
| 86 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 87 |
+
|
| 88 |
+
return UserResponse(**user)
|
| 89 |
+
|
| 90 |
+
except HTTPException:
|
| 91 |
+
raise
|
| 92 |
+
except Exception as e:
|
| 93 |
+
logger.error(f"Error getting user: {str(e)}")
|
| 94 |
+
raise HTTPException(status_code=500, detail=f"Failed to get user: {str(e)}")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@router.patch("/me/name", response_model=UserResponse, tags=["Users"])
|
| 98 |
+
async def update_user_name(
|
| 99 |
+
request: UserUpdateNameRequest,
|
| 100 |
+
x_user_id: Optional[str] = Header(None, alias="X-User-ID")
|
| 101 |
+
):
|
| 102 |
+
"""
|
| 103 |
+
Update user's display name
|
| 104 |
+
|
| 105 |
+
- **X-User-ID**: User UUID (sent in request header)
|
| 106 |
+
- **name**: New display name
|
| 107 |
+
|
| 108 |
+
Returns updated user data
|
| 109 |
+
"""
|
| 110 |
+
if not x_user_id:
|
| 111 |
+
raise HTTPException(status_code=400, detail="X-User-ID header is required")
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
user = user_service.update_user_name(
|
| 115 |
+
user_id=x_user_id,
|
| 116 |
+
name=request.name
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
logger.info(f"Updated user name: {x_user_id}")
|
| 120 |
+
return UserResponse(**user)
|
| 121 |
+
|
| 122 |
+
except ValueError as e:
|
| 123 |
+
logger.error(f"Validation error: {str(e)}")
|
| 124 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 125 |
+
except Exception as e:
|
| 126 |
+
logger.error(f"Update error: {str(e)}")
|
| 127 |
+
raise HTTPException(status_code=500, detail=f"Update failed: {str(e)}")
|
| 128 |
+
|
services/__init__.py
CHANGED
|
@@ -8,6 +8,8 @@ from .generate_model_manager import GenerateModelManager, generate_model_manager
|
|
| 8 |
from .stt_service import speech_to_text
|
| 9 |
from .tts_service import text_to_speech
|
| 10 |
from .topic_service import TopicService, topic_service
|
|
|
|
|
|
|
| 11 |
|
| 12 |
__all__ = [
|
| 13 |
"StanceModelManager",
|
|
@@ -18,6 +20,10 @@ __all__ = [
|
|
| 18 |
"generate_model_manager",
|
| 19 |
"TopicService",
|
| 20 |
"topic_service",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# NEW exports
|
| 23 |
"speech_to_text",
|
|
|
|
| 8 |
from .stt_service import speech_to_text
|
| 9 |
from .tts_service import text_to_speech
|
| 10 |
from .topic_service import TopicService, topic_service
|
| 11 |
+
from .database_service import DatabaseService, database_service
|
| 12 |
+
from .user_service import UserService, user_service
|
| 13 |
|
| 14 |
__all__ = [
|
| 15 |
"StanceModelManager",
|
|
|
|
| 20 |
"generate_model_manager",
|
| 21 |
"TopicService",
|
| 22 |
"topic_service",
|
| 23 |
+
"DatabaseService",
|
| 24 |
+
"database_service",
|
| 25 |
+
"UserService",
|
| 26 |
+
"user_service",
|
| 27 |
|
| 28 |
# NEW exports
|
| 29 |
"speech_to_text",
|
services/database_service.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Database service for Supabase connection"""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
from supabase import create_client, Client
|
| 5 |
+
from config import SUPABASE_URL, SUPABASE_KEY
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class DatabaseService:
|
| 11 |
+
"""Service for managing Supabase database connection"""
|
| 12 |
+
|
| 13 |
+
def __init__(self):
|
| 14 |
+
self.client: Client | None = None
|
| 15 |
+
self.connected = False
|
| 16 |
+
|
| 17 |
+
def connect(self):
|
| 18 |
+
"""Initialize Supabase client connection"""
|
| 19 |
+
if self.connected and self.client:
|
| 20 |
+
logger.info("Database already connected")
|
| 21 |
+
return
|
| 22 |
+
|
| 23 |
+
if not SUPABASE_URL or not SUPABASE_KEY:
|
| 24 |
+
raise ValueError("SUPABASE_URL and SUPABASE_KEY must be set in environment variables")
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
logger.info("Connecting to Supabase...")
|
| 28 |
+
self.client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
| 29 |
+
self.connected = True
|
| 30 |
+
logger.info("✓ Successfully connected to Supabase")
|
| 31 |
+
except Exception as e:
|
| 32 |
+
logger.error(f"Error connecting to Supabase: {str(e)}")
|
| 33 |
+
raise RuntimeError(f"Failed to connect to Supabase: {str(e)}")
|
| 34 |
+
|
| 35 |
+
def get_client(self) -> Client:
|
| 36 |
+
"""Get Supabase client instance"""
|
| 37 |
+
if not self.connected or not self.client:
|
| 38 |
+
self.connect()
|
| 39 |
+
return self.client
|
| 40 |
+
|
| 41 |
+
def is_connected(self) -> bool:
|
| 42 |
+
"""Check if database is connected"""
|
| 43 |
+
return self.connected and self.client is not None
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# Initialize singleton instance
|
| 47 |
+
database_service = DatabaseService()
|
| 48 |
+
|
services/user_service.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""User service for managing user operations"""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
import uuid
|
| 5 |
+
from typing import Optional, Dict
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
|
| 8 |
+
from services.database_service import database_service
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class UserService:
|
| 14 |
+
"""Service for user CRUD operations"""
|
| 15 |
+
|
| 16 |
+
def __init__(self):
|
| 17 |
+
self.table_name = "users"
|
| 18 |
+
|
| 19 |
+
def _get_client(self):
|
| 20 |
+
"""Get Supabase client"""
|
| 21 |
+
return database_service.get_client()
|
| 22 |
+
|
| 23 |
+
def create_user(self, unique_id: str, name: str) -> Dict:
|
| 24 |
+
"""
|
| 25 |
+
Create a new user or get existing user by unique_id
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
unique_id: Browser-generated unique identifier
|
| 29 |
+
name: User's display name
|
| 30 |
+
|
| 31 |
+
Returns:
|
| 32 |
+
User dictionary with id, unique_id, name, created_at, updated_at
|
| 33 |
+
"""
|
| 34 |
+
try:
|
| 35 |
+
client = self._get_client()
|
| 36 |
+
|
| 37 |
+
# Check if user already exists
|
| 38 |
+
existing_user = self.get_user_by_unique_id(unique_id)
|
| 39 |
+
if existing_user:
|
| 40 |
+
logger.info(f"User already exists with unique_id: {unique_id}")
|
| 41 |
+
return existing_user
|
| 42 |
+
|
| 43 |
+
# Create new user
|
| 44 |
+
user_data = {
|
| 45 |
+
"unique_id": unique_id,
|
| 46 |
+
"name": name,
|
| 47 |
+
"created_at": datetime.utcnow().isoformat(),
|
| 48 |
+
"updated_at": datetime.utcnow().isoformat()
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
result = client.table(self.table_name).insert(user_data).execute()
|
| 52 |
+
|
| 53 |
+
if result.data and len(result.data) > 0:
|
| 54 |
+
logger.info(f"Created new user: {result.data[0].get('id')}")
|
| 55 |
+
return result.data[0]
|
| 56 |
+
else:
|
| 57 |
+
raise RuntimeError("Failed to create user: no data returned")
|
| 58 |
+
|
| 59 |
+
except Exception as e:
|
| 60 |
+
logger.error(f"Error creating user: {str(e)}")
|
| 61 |
+
raise RuntimeError(f"Failed to create user: {str(e)}")
|
| 62 |
+
|
| 63 |
+
def get_user_by_unique_id(self, unique_id: str) -> Optional[Dict]:
|
| 64 |
+
"""
|
| 65 |
+
Get user by unique_id
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
unique_id: Browser-generated unique identifier
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
User dictionary or None if not found
|
| 72 |
+
"""
|
| 73 |
+
try:
|
| 74 |
+
client = self._get_client()
|
| 75 |
+
|
| 76 |
+
result = client.table(self.table_name).select("*").eq("unique_id", unique_id).execute()
|
| 77 |
+
|
| 78 |
+
if result.data and len(result.data) > 0:
|
| 79 |
+
return result.data[0]
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
except Exception as e:
|
| 83 |
+
logger.error(f"Error getting user by unique_id: {str(e)}")
|
| 84 |
+
raise RuntimeError(f"Failed to get user: {str(e)}")
|
| 85 |
+
|
| 86 |
+
def get_user_by_id(self, user_id: str) -> Optional[Dict]:
|
| 87 |
+
"""
|
| 88 |
+
Get user by id (UUID)
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
user_id: User UUID
|
| 92 |
+
|
| 93 |
+
Returns:
|
| 94 |
+
User dictionary or None if not found
|
| 95 |
+
"""
|
| 96 |
+
try:
|
| 97 |
+
client = self._get_client()
|
| 98 |
+
|
| 99 |
+
result = client.table(self.table_name).select("*").eq("id", user_id).execute()
|
| 100 |
+
|
| 101 |
+
if result.data and len(result.data) > 0:
|
| 102 |
+
return result.data[0]
|
| 103 |
+
return None
|
| 104 |
+
|
| 105 |
+
except Exception as e:
|
| 106 |
+
logger.error(f"Error getting user by id: {str(e)}")
|
| 107 |
+
raise RuntimeError(f"Failed to get user: {str(e)}")
|
| 108 |
+
|
| 109 |
+
def update_user_name(self, user_id: str, name: str) -> Dict:
|
| 110 |
+
"""
|
| 111 |
+
Update user's name
|
| 112 |
+
|
| 113 |
+
Args:
|
| 114 |
+
user_id: User UUID
|
| 115 |
+
name: New display name
|
| 116 |
+
|
| 117 |
+
Returns:
|
| 118 |
+
Updated user dictionary
|
| 119 |
+
"""
|
| 120 |
+
try:
|
| 121 |
+
client = self._get_client()
|
| 122 |
+
|
| 123 |
+
update_data = {
|
| 124 |
+
"name": name,
|
| 125 |
+
"updated_at": datetime.utcnow().isoformat()
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
result = client.table(self.table_name).update(update_data).eq("id", user_id).execute()
|
| 129 |
+
|
| 130 |
+
if result.data and len(result.data) > 0:
|
| 131 |
+
logger.info(f"Updated user name: {user_id}")
|
| 132 |
+
return result.data[0]
|
| 133 |
+
else:
|
| 134 |
+
raise RuntimeError("Failed to update user: user not found")
|
| 135 |
+
|
| 136 |
+
except Exception as e:
|
| 137 |
+
logger.error(f"Error updating user name: {str(e)}")
|
| 138 |
+
raise RuntimeError(f"Failed to update user: {str(e)}")
|
| 139 |
+
|
| 140 |
+
def register_or_get_user(self, unique_id: str, name: Optional[str] = None) -> Dict:
|
| 141 |
+
"""
|
| 142 |
+
Register a new user or get existing user by unique_id
|
| 143 |
+
If name is not provided, generates a random one
|
| 144 |
+
|
| 145 |
+
Args:
|
| 146 |
+
unique_id: Browser-generated unique identifier
|
| 147 |
+
name: Optional user's display name (generates random if not provided)
|
| 148 |
+
|
| 149 |
+
Returns:
|
| 150 |
+
User dictionary
|
| 151 |
+
"""
|
| 152 |
+
# Generate random name if not provided
|
| 153 |
+
if not name:
|
| 154 |
+
name = f"User_{uuid.uuid4().hex[:8]}"
|
| 155 |
+
|
| 156 |
+
# Check if user exists
|
| 157 |
+
existing_user = self.get_user_by_unique_id(unique_id)
|
| 158 |
+
if existing_user:
|
| 159 |
+
return existing_user
|
| 160 |
+
|
| 161 |
+
# Create new user
|
| 162 |
+
return self.create_user(unique_id, name)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
# Initialize singleton instance
|
| 166 |
+
user_service = UserService()
|
| 167 |
+
|