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
3 changes: 2 additions & 1 deletion backend/app/api/v1/api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from fastapi import APIRouter
from app.api.v1.endpoints import auth, settings, ollama, ai_providers, ai_provider_settings, navigation_routes, components, conversations, tags, personas, plugin_state
from app.api.v1.endpoints import auth, settings, ollama, ai_providers, ai_provider_settings, navigation_routes, components, conversations, tags, personas, plugin_state, demo
from app.routers import plugins
from app.routes.pages import router as pages_router

Expand All @@ -15,6 +15,7 @@
api_router.include_router(tags.router, tags=["tags"])
api_router.include_router(personas.router, tags=["personas"])
api_router.include_router(plugin_state.router, tags=["plugin-state"])
api_router.include_router(demo.router, tags=["demo"])
# Include the plugins router (which already includes the lifecycle router)
api_router.include_router(plugins.router, tags=["plugins"])
api_router.include_router(pages_router)
219 changes: 219 additions & 0 deletions backend/app/api/v1/endpoints/demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
"""
Demo API endpoints for ServiceExample_API plugin demonstration.

This module provides simple CRUD operations for demonstration purposes.
Uses in-memory storage (no database required) and requires user authentication.
"""

from fastapi import APIRouter, Depends, HTTPException
from typing import List, Dict, Any, Optional
from datetime import datetime
from pydantic import BaseModel, Field
from app.core.security import get_current_user
from app.models.user import User

router = APIRouter()

# In-memory storage for demo purposes
demo_items: Dict[str, Dict[str, Any]] = {}
item_counter = 0

class CreateItemRequest(BaseModel):
"""Request model for creating a new demo item."""
name: str = Field(..., min_length=1, max_length=100, description="Item name")
description: str = Field("", max_length=500, description="Item description")

class UpdateItemRequest(BaseModel):
"""Request model for updating an existing demo item."""
name: Optional[str] = Field(None, min_length=1, max_length=100, description="Item name")
description: Optional[str] = Field(None, max_length=500, description="Item description")

class DemoItemResponse(BaseModel):
"""Response model for demo items."""
id: str
name: str
description: str
user_id: str
created_at: str
updated_at: str

@router.get("/demo/items", response_model=Dict[str, Any])
async def get_demo_items(current_user: User = Depends(get_current_user)):
"""
Get all demo items for the current user.

Returns:
Dict containing data array and count of items
"""
try:
user_items = {k: v for k, v in demo_items.items() if v.get("user_id") == current_user.id}
items_list = list(user_items.values())

return {
"data": items_list,
"count": len(items_list),
"message": f"Retrieved {len(items_list)} items successfully"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to retrieve items: {str(e)}")

@router.post("/demo/items", response_model=Dict[str, Any])
async def create_demo_item(
item_data: CreateItemRequest,
current_user: User = Depends(get_current_user)
):
"""
Create a new demo item.

Args:
item_data: The item data to create
current_user: The authenticated user

Returns:
Dict containing the created item and success message
"""
global item_counter

try:
item_counter += 1

item = {
"id": str(item_counter),
"name": item_data.name.strip(),
"description": item_data.description.strip(),
"user_id": current_user.id,
"created_at": datetime.utcnow().isoformat(),
"updated_at": datetime.utcnow().isoformat()
}

demo_items[str(item_counter)] = item

return {
"data": item,
"message": "Item created successfully"
}

except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to create item: {str(e)}")

@router.put("/demo/items/{item_id}", response_model=Dict[str, Any])
async def update_demo_item(
item_id: str,
item_data: UpdateItemRequest,
current_user: User = Depends(get_current_user)
):
"""
Update an existing demo item.

Args:
item_id: The ID of the item to update
item_data: The updated item data
current_user: The authenticated user

Returns:
Dict containing the updated item and success message
"""
try:
if item_id not in demo_items:
raise HTTPException(status_code=404, detail="Item not found")

item = demo_items[item_id]
if item.get("user_id") != current_user.id:
raise HTTPException(status_code=403, detail="Not authorized to update this item")

# Update only provided fields
if item_data.name is not None:
if not item_data.name.strip():
raise HTTPException(status_code=400, detail="Item name cannot be empty")
item["name"] = item_data.name.strip()

if item_data.description is not None:
item["description"] = item_data.description.strip()

item["updated_at"] = datetime.utcnow().isoformat()

return {
"data": item,
"message": "Item updated successfully"
}

except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to update item: {str(e)}")

@router.delete("/demo/items/{item_id}", response_model=Dict[str, Any])
async def delete_demo_item(
item_id: str,
current_user: User = Depends(get_current_user)
):
"""
Delete a demo item.

Args:
item_id: The ID of the item to delete
current_user: The authenticated user

Returns:
Dict containing success message
"""
try:
if item_id not in demo_items:
raise HTTPException(status_code=404, detail="Item not found")

item = demo_items[item_id]
if item.get("user_id") != current_user.id:
raise HTTPException(status_code=403, detail="Not authorized to delete this item")

del demo_items[item_id]

return {
"message": "Item deleted successfully",
"deleted_item_id": item_id
}

except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to delete item: {str(e)}")

@router.get("/demo/status", response_model=Dict[str, Any])
async def get_demo_status(current_user: User = Depends(get_current_user)):
"""
Get demo API status and statistics.

Args:
current_user: The authenticated user

Returns:
Dict containing status information and statistics
"""
try:
user_items = {k: v for k, v in demo_items.items() if v.get("user_id") == current_user.id}

return {
"status": "active",
"user_item_count": len(user_items),
"total_items": len(demo_items),
"server_time": datetime.utcnow().isoformat(),
"user_id": current_user.id,
"message": "Demo API is running successfully"
}

except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get status: {str(e)}")

@router.get("/demo/health", response_model=Dict[str, Any])
async def health_check():
"""
Simple health check endpoint (no authentication required).

Returns:
Dict containing health status
"""
return {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"service": "demo-api",
"version": "1.0.0"
}
80 changes: 53 additions & 27 deletions backend/app/api/v1/endpoints/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from app.core.security import get_current_user
from app.models.settings import SettingDefinition, SettingInstance, SettingScope
from app.models.user import User
from app.models.page import Page
# Import all models to ensure they're registered with SQLAlchemy
from app.models import *
from app.schemas.settings import (
SettingDefinitionCreate,
SettingDefinitionUpdate,
Expand Down Expand Up @@ -707,6 +710,11 @@ async def create_setting_instance(
detail=f"Error updating instance: {str(e)}"
)

# Handle special case for 'current' user_id BEFORE duplicate check
if instance_data.user_id == 'current' and current_user:
logger.info(f"Resolving 'current' user_id to actual user ID: {current_user.id}")
instance_data.user_id = str(current_user.id)

# Ensure user context is set correctly
if instance_data.scope == SettingScope.USER or instance_data.scope == SettingScope.USER_PAGE:
if not instance_data.user_id and current_user:
Expand Down Expand Up @@ -871,9 +879,11 @@ async def create_setting_instance(
user_id_value = str(current_user.id)

# Convert value to JSON string if it's not already
value_json = instance_data.value
if not isinstance(value_json, str):
value_json = json.dumps(value_json)
value_data = instance_data.value
if not isinstance(value_data, str):
value_json = json.dumps(value_data)
else:
value_json = value_data

# Convert scope to string if it's an enum
scope_value = instance_data.scope
Expand Down Expand Up @@ -901,35 +911,51 @@ async def create_setting_instance(
else:
scope_enum = scope_value

# Create new instance using SQLAlchemy model
new_instance = SettingInstance(
id=instance_id,
definition_id=instance_data.definition_id,
name=instance_data.name,
value=instance_data.value, # This will be automatically encrypted
scope=scope_enum,
user_id=user_id_value,
page_id=instance_data.page_id
)
# Use raw SQL to bypass SQLAlchemy foreign key resolution issues
from datetime import datetime

# Prepare the values for raw SQL insertion
now = datetime.utcnow()
# Use the already converted value_json from above
scope_str = scope_enum.value if hasattr(scope_enum, 'value') else str(scope_enum)

# Insert using raw SQL
insert_query = text("""
INSERT INTO settings_instances
(id, definition_id, name, value, scope, user_id, page_id, created_at, updated_at)
VALUES (:id, :definition_id, :name, :value, :scope, :user_id, :page_id, :created_at, :updated_at)
""")

await db.execute(insert_query, {
'id': instance_id,
'definition_id': instance_data.definition_id,
'name': instance_data.name,
'value': value_json,
'scope': scope_str,
'user_id': user_id_value,
'page_id': instance_data.page_id,
'created_at': now,
'updated_at': now
})

# Add and commit the instance
db.add(new_instance)
await db.commit()
await db.refresh(new_instance)

# Convert to dict for response
created_instance = {
"id": new_instance.id,
"definition_id": new_instance.definition_id,
"name": new_instance.name,
"value": new_instance.value, # This will be automatically decrypted
"scope": new_instance.scope.value if hasattr(new_instance.scope, 'value') else new_instance.scope,
"user_id": new_instance.user_id,
"page_id": new_instance.page_id,
"created_at": new_instance.created_at,
"updated_at": new_instance.updated_at
# Create a response object manually
new_instance = {
'id': instance_id,
'definition_id': instance_data.definition_id,
'name': instance_data.name,
'value': instance_data.value,
'scope': scope_str,
'user_id': user_id_value,
'page_id': instance_data.page_id,
'created_at': now,
'updated_at': now
}

# new_instance is already a dict, so use it directly as the response
created_instance = new_instance

logger.info(f"Created setting instance: {instance_id}")
return created_instance
except Exception as e:
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/services/PluginStateFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class PluginStateFactoryImpl extends AbstractBaseService implements PluginStateF
const service = createPluginStateService(pluginId);
this.activeServices.set(pluginId, service);

// Initialize the service
// Initialize the service asynchronously but don't wait
service.initialize().catch(error => {
console.error(`[PluginStateFactory] Error initializing plugin state service for ${pluginId}:`, error);
});
Expand Down
Loading