Commit 584f3c29 authored by Your Name's avatar Your Name

Fix user endpoints

parent 9678cbc4
......@@ -1029,14 +1029,26 @@ Authorization: Bearer YOUR_API_TOKEN
#### User API Endpoints
Authenticated users can access their configurations using their username in the endpoint path. Each user has their own dedicated API endpoints:
| Endpoint | Description |
|----------|-------------|
| `GET /api/user/models` | List available models from user's own configurations |
| `GET /api/user/providers` | List user's provider configurations |
| `GET /api/user/rotations` | List user's rotation configurations |
| `GET /api/user/autoselects` | List user's autoselect configurations |
| `POST /api/user/chat/completions` | Chat completions using user's own models |
| `GET /api/user/{config_type}/models` | List models for specific config type (provider, rotation, autoselect) |
| `GET /api/u/{username}/models` | List available models from user's own configurations |
| `GET /api/u/{username}/providers` | List user's provider configurations |
| `GET /api/u/{username}/rotations` | List user's rotation configurations |
| `GET /api/u/{username}/autoselects` | List user's autoselect configurations |
| `POST /api/u/{username}/chat/completions` | Chat completions using user's own models |
| `GET /api/u/{username}/{config_type}/models` | List models for specific config type (provider, rotation, autoselect) |
Legacy `/api/user/...` endpoints remain fully supported for backward compatibility:
| Legacy Endpoint | Description |
|-----------------|-------------|
| `GET /api/user/models` | Legacy endpoint for authenticated user |
| `GET /api/user/providers` | Legacy endpoint for authenticated user |
| `GET /api/user/rotations` | Legacy endpoint for authenticated user |
| `GET /api/user/autoselects` | Legacy endpoint for authenticated user |
| `POST /api/user/chat/completions` | Legacy chat completions endpoint |
| `GET /api/user/{config_type}/models` | Legacy model listing endpoint |
#### Access Control
......@@ -1058,22 +1070,34 @@ Users can create and manage API tokens through the dashboard:
#### Example: Using User API with cURL
```bash
# List user models
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/models
# List models for username 'johnsmith'
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/u/johnsmith/models
# Chat using user's own models
# Chat using johnsmith's models
curl -X POST -H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model": "your-rotation/model", "messages": [{"role": "user", "content": "Hello"}]}' \
http://localhost:17765/api/user/chat/completions
http://localhost:17765/api/u/johnsmith/chat/completions
# List user's providers
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/providers
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/u/johnsmith/providers
# List user's rotations
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/user/rotations
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:17765/api/u/johnsmith/rotations
```
#### User Dashboard
When logged into the user dashboard, each user will see their own personal endpoint URLs displayed with their username automatically inserted. Users can copy their endpoint URLs directly from the dashboard homepage.
#### Access Permissions
- **Users** may only access their own personal endpoints
- **Administrators** may access any user's endpoints
- **Global API Tokens** may access any user's endpoints
All requests are properly authenticated and authorized using the standard Bearer token authentication system.
#### MCP Integration
User tokens also work with MCP (Model Context Protocol) endpoints:
......
......@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model
__version__ = "0.99.16"
__version__ = "0.99.19"
__all__ = [
# Config
"config",
......
......@@ -270,6 +270,19 @@ class DatabaseManager:
)
''')
cursor.execute(f'''
CREATE TABLE IF NOT EXISTS user_prompts (
id INTEGER PRIMARY KEY {auto_increment},
user_id INTEGER NOT NULL,
prompt_key VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT {timestamp_default},
updated_at TIMESTAMP DEFAULT {timestamp_default},
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE(user_id, prompt_key)
)
''')
cursor.execute(f'''
CREATE TABLE IF NOT EXISTS user_api_tokens (
id INTEGER PRIMARY KEY {auto_increment},
......@@ -756,6 +769,35 @@ class DatabaseManager:
}
return None
def get_user_by_username(self, username: str) -> Optional[Dict]:
"""
Get a user by username.
Args:
username: Username to look up
Returns:
User dict if found, None otherwise
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
SELECT id, username, role, is_active
FROM users
WHERE username = {placeholder} AND is_active = 1
''', (username,))
row = cursor.fetchone()
if row:
return {
'id': row[0],
'username': row[1],
'role': row[2],
'is_active': row[3]
}
return None
def create_user(self, username: str, password_hash: str, role: str = 'user', created_by: str = None) -> int:
"""
Create a new user.
......@@ -1179,6 +1221,103 @@ class DatabaseManager:
''', (user_id, autoselect_name))
conn.commit()
# User-specific prompt methods
def save_user_prompt(self, user_id: int, prompt_key: str, content: str):
"""
Save user-specific prompt override.
Args:
user_id: User ID
prompt_key: Prompt identifier
content: Prompt content
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
if self.db_type == 'sqlite':
cursor.execute(f'''
INSERT OR REPLACE INTO user_prompts (user_id, prompt_key, content, updated_at)
VALUES ({placeholder}, {placeholder}, {placeholder}, CURRENT_TIMESTAMP)
''', (user_id, prompt_key, content))
else: # mysql
cursor.execute(f'''
INSERT INTO user_prompts (user_id, prompt_key, content, updated_at)
VALUES ({placeholder}, {placeholder}, {placeholder}, CURRENT_TIMESTAMP)
ON DUPLICATE KEY UPDATE content=VALUES(content), updated_at=CURRENT_TIMESTAMP
''', (user_id, prompt_key, content))
conn.commit()
def get_user_prompt(self, user_id: int, prompt_key: str) -> Optional[str]:
"""
Get user-specific prompt override.
Args:
user_id: User ID
prompt_key: Prompt identifier
Returns:
Prompt content if exists, None otherwise
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
SELECT content
FROM user_prompts
WHERE user_id = {placeholder} AND prompt_key = {placeholder}
''', (user_id, prompt_key))
row = cursor.fetchone()
return row[0] if row else None
def get_user_prompts(self, user_id: int) -> List[Dict]:
"""
Get all user-specific prompt overrides for a user.
Args:
user_id: User ID
Returns:
List of prompt dictionaries
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
SELECT prompt_key, content, created_at, updated_at
FROM user_prompts
WHERE user_id = {placeholder}
ORDER BY prompt_key
''', (user_id,))
prompts = []
for row in cursor.fetchall():
prompts.append({
'prompt_key': row[0],
'content': row[1],
'created_at': row[2],
'updated_at': row[3]
})
return prompts
def delete_user_prompt(self, user_id: int, prompt_key: str):
"""
Delete user-specific prompt override.
Args:
user_id: User ID
prompt_key: Prompt identifier
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
cursor.execute(f'''
DELETE FROM user_prompts
WHERE user_id = {placeholder} AND prompt_key = {placeholder}
''', (user_id, prompt_key))
conn.commit()
# User API token methods
def create_user_api_token(self, user_id: int, token: str, description: str = None) -> int:
"""
......
This diff is collapsed.
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aisbf"
version = "0.99.18"
version = "0.99.19"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md"
license = "GPL-3.0-or-later"
......
......@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.99.17",
version="0.99.19",
author="AISBF Contributors",
author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......
......@@ -418,8 +418,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<a href="{{ url_for(request, '/dashboard/analytics') }}" {% if '/analytics' in request.path %}class="active"{% endif %}>Analytics</a>
{% if request.session.role == 'admin' %}
<a href="{{ url_for(request, '/dashboard/users') }}" {% if '/users' in request.path %}class="active"{% endif %}>Users</a>
{% endif %}
<a href="{{ url_for(request, '/dashboard/settings') }}" {% if '/settings' in request.path %}class="active"{% endif %}>Settings</a>
{% endif %}
<a href="{{ url_for(request, '/dashboard/docs') }}" {% if '/docs' in request.path %}class="active"{% endif %}>Docs</a>
<a href="{{ url_for(request, '/dashboard/about') }}" {% if '/about' in request.path %}class="active"{% endif %}>About</a>
</div>
......
......@@ -49,6 +49,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button type="submit" class="btn">Save Prompt</button>
{% if not is_admin %}
<button type="button" class="btn btn-secondary" onclick="resetPrompt()">Reset to Default</button>
{% endif %}
<a href="/dashboard" class="btn btn-secondary">Cancel</a>
</div>
</form>
......@@ -69,6 +72,22 @@ function switchPrompt() {
}
}
function resetPrompt() {
if (!currentPromptKey) return;
if (confirm('Are you sure you want to reset this prompt to the default admin configuration?')) {
fetch('/dashboard/prompts/reset/' + currentPromptKey, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
}).then(response => {
if (response.ok) {
location.reload();
}
});
}
}
// Initialize with first prompt
if (prompts.length > 0) {
currentPromptKey = prompts[0].key;
......
......@@ -25,35 +25,35 @@
<h3>Your Models</h3>
<div class="endpoint">
<code class="method GET">GET</code>
<code class="url">/api/user/models</code>
<code class="url">{{ get_base_url(request) }}/api/u/{{ session.username }}/models</code>
<p>List all models from your providers, rotations, and autoselects</p>
</div>
<h3>Your Providers</h3>
<div class="endpoint">
<code class="method GET">GET</code>
<code class="url">/api/user/providers</code>
<code class="url">{{ get_base_url(request) }}/api/u/{{ session.username }}/providers</code>
<p>List all your configured providers</p>
</div>
<h3>Your Rotations</h3>
<div class="endpoint">
<code class="method GET">GET</code>
<code class="url">/api/user/rotations</code>
<code class="url">{{ get_base_url(request) }}/api/u/{{ session.username }}/rotations</code>
<p>List all your configured rotations</p>
</div>
<h3>Your Autoselects</h3>
<div class="endpoint">
<code class="method GET">GET</code>
<code class="url">/api/user/autoselects</code>
<code class="url">{{ get_base_url(request) }}/api/u/{{ session.username }}/autoselects</code>
<p>List all your configured autoselects</p>
</div>
<h3>Your Chat Completions</h3>
<div class="endpoint">
<code class="method POST">POST</code>
<code class="url">/api/user/chat/completions</code>
<code class="url">{{ get_base_url(request) }}/api/u/{{ session.username }}/chat/completions</code>
<p>Send chat requests using your configurations</p>
<p class="example">Example model formats:</p>
<ul>
......@@ -66,12 +66,12 @@
<h3>MCP Tools</h3>
<div class="endpoint">
<code class="method GET">GET</code>
<code class="url">/mcp/tools</code>
<code class="url">{{ get_base_url(request) }}/mcp/tools</code>
<p>List available MCP tools for your configurations</p>
</div>
<div class="endpoint">
<code class="method POST">POST</code>
<code class="url">/mcp/tools/call</code>
<code class="url">{{ get_base_url(request) }}/mcp/tools/call</code>
<p>Call MCP tools to manage your configurations</p>
</div>
</div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment