Fix database creations

parent d5565050
This diff is collapsed.
......@@ -1504,6 +1504,20 @@ async def api_token_authorization_middleware(request: Request, call_next):
"requested_user": target_username
}
)
# Enforce token scope
token_scope = getattr(request.state, 'token_scope', 'both')
is_mcp_path = path.startswith("/mcp/u/") or path.startswith("/mcp/v1/u/")
if is_mcp_path and token_scope == 'api':
return JSONResponse(
status_code=403,
content={"error": "This token does not have MCP access. Create a token with 'mcp' or 'both' scope."}
)
if not is_mcp_path and token_scope == 'mcp':
return JSONResponse(
status_code=403,
content={"error": "This token does not have API access. Create a token with 'api' or 'both' scope."}
)
# --- GLOBAL ENDPOINTS (all other API paths) ---
else:
......@@ -1557,6 +1571,7 @@ async def auth_middleware(request: Request, call_next):
request.state.user_id = None
request.state.token_id = None
request.state.is_global_token = True
request.state.token_scope = 'api' # global tokens are API-scope by default
request.state.is_admin = True # Global tokens have admin access
else:
# Check user API tokens
......@@ -1568,6 +1583,7 @@ async def auth_middleware(request: Request, call_next):
request.state.user_id = user_auth['user_id']
request.state.token_id = user_auth['token_id']
request.state.is_global_token = False
request.state.token_scope = user_auth.get('scope', 'api')
# Store user role - admin users get full access
request.state.is_admin = (user_auth.get('role') == 'admin')
else:
......@@ -1580,6 +1596,7 @@ async def auth_middleware(request: Request, call_next):
request.state.user_id = None
request.state.token_id = None
request.state.is_global_token = False
request.state.token_scope = 'both'
# Check for unverified email for logged in dashboard users
# Only enforce email verification if:
......@@ -6771,7 +6788,7 @@ async def dashboard_user_tokens(request: Request):
)
@app.post("/dashboard/user/tokens")
async def dashboard_user_tokens_create(request: Request, description: str = Form("")):
async def dashboard_user_tokens_create(request: Request, description: str = Form(""), scope: str = Form("api")):
"""Create a new user API token"""
auth_check = require_dashboard_auth(request)
if auth_check:
......@@ -6781,6 +6798,9 @@ async def dashboard_user_tokens_create(request: Request, description: str = Form
if not user_id:
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
if scope not in ('api', 'mcp', 'both'):
scope = 'api'
import secrets
db = DatabaseRegistry.get_config_database()
......@@ -6789,11 +6809,12 @@ async def dashboard_user_tokens_create(request: Request, description: str = Form
token = secrets.token_urlsafe(32)
try:
token_id = db.create_user_api_token(user_id, token, description.strip() or None)
token_id = db.create_user_api_token(user_id, token, description.strip() or None, scope)
return JSONResponse({
"message": "Token created successfully",
"token": token,
"token_id": token_id
"token_id": token_id,
"scope": scope
})
except Exception as e:
return JSONResponse(status_code=500, content={"error": str(e)})
......@@ -8320,6 +8341,29 @@ async def dashboard_wallet_transactions(request: Request, limit: int = 50, offse
return JSONResponse({"error": "Failed to load transactions"}, status_code=500)
@app.put("/dashboard/wallet/auto-topup")
async def dashboard_wallet_auto_topup(request: Request):
"""Session-authenticated auto-topup configuration (used by the wallet dashboard page)."""
auth_check = require_dashboard_auth(request)
if auth_check:
from fastapi.responses import JSONResponse
return JSONResponse({"error": "Unauthorized"}, status_code=401)
user_id = request.session.get('user_id')
try:
body = await request.json()
from aisbf.payments.wallet.manager import WalletManager
db = DatabaseRegistry.get_config_database()
wallet_manager = WalletManager(db)
result = await wallet_manager.configure_auto_topup(user_id, body)
from fastapi.responses import JSONResponse
return JSONResponse(result)
except Exception as e:
logger.error(f"Failed to configure auto-topup: {e}")
from fastapi.responses import JSONResponse
return JSONResponse({"error": "Failed to save settings"}, status_code=500)
@app.get("/dashboard/billing")
async def dashboard_billing(request: Request):
"""User payment transaction history page"""
......
......@@ -276,6 +276,22 @@
<label>Description <span style="color:#555;">(optional)</span></label>
<input type="text" id="tokenDescription" placeholder="e.g. My app, Home server …">
</div>
<div class="form-group">
<label>Scope</label>
<div style="display:flex; gap:.75rem; flex-wrap:wrap; margin-top:.3rem;">
<label style="display:flex; align-items:center; gap:.4rem; cursor:pointer; color:#c0c0c0; font-size:.88rem;">
<input type="radio" name="tokenScope" value="api" checked> API only
<span style="color:#555; font-size:.78rem;">(proxy requests)</span>
</label>
<label style="display:flex; align-items:center; gap:.4rem; cursor:pointer; color:#c0c0c0; font-size:.88rem;">
<input type="radio" name="tokenScope" value="mcp"> MCP only
<span style="color:#555; font-size:.78rem;">(agent tools)</span>
</label>
<label style="display:flex; align-items:center; gap:.4rem; cursor:pointer; color:#c0c0c0; font-size:.88rem;">
<input type="radio" name="tokenScope" value="both"> Both
</label>
</div>
</div>
<div class="form-actions">
<button class="btn btn-primary btn-sm" onclick="submitCreateToken()">Create</button>
<button class="btn btn-secondary btn-sm" onclick="toggleCreateForm()">Cancel</button>
......@@ -309,17 +325,27 @@
<p style="color:#a0a0a0; font-size:.88rem; margin-bottom:.75rem;">Add the token to every request in the <code style="background:#0f3460; padding:.1rem .35rem; border-radius:3px;">Authorization</code> header:</p>
<div class="code-block">Authorization: Bearer YOUR_API_TOKEN</div>
<p style="color:#a0a0a0; font-size:.88rem; margin-top:1.25rem; margin-bottom:.5rem;">Token scopes:</p>
<table class="ep-table">
<thead><tr><th>Scope</th><th>Access</th></tr></thead>
<tbody>
<tr><td><code style="color:#60a5fa;">api</code></td><td>Proxy API endpoints only (<code>/api/u/…</code>)</td></tr>
<tr><td><code style="color:#a78bfa;">mcp</code></td><td>MCP tool endpoints only (<code>/mcp/u/…</code>)</td></tr>
<tr><td><code style="color:#4ade80;">both</code></td><td>Both API and MCP endpoints</td></tr>
</tbody>
</table>
<p style="color:#a0a0a0; font-size:.88rem; margin-top:1.25rem; margin-bottom:.5rem;">Available endpoints:</p>
<table class="ep-table">
<thead><tr><th>Method</th><th>Endpoint</th><th>Description</th></tr></thead>
<thead><tr><th>Method</th><th>Endpoint</th><th>Scope</th><th>Description</th></tr></thead>
<tbody>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/models</code></td> <td>List your models</td></tr>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/providers</code></td> <td>List your providers</td></tr>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/rotations</code></td> <td>List your rotations</td></tr>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/autoselects</code></td> <td>List your autoselects</td></tr>
<tr><td><span class="method-badge method-POST">POST</span></td><td><code>/api/u/{{ session.username }}/chat/completions</code></td><td>Chat using your configs</td></tr>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/mcp/u/{{ session.username }}/tools</code></td> <td>List MCP tools</td></tr>
<tr><td><span class="method-badge method-POST">POST</span></td><td><code>/mcp/u/{{ session.username }}/tools/call</code></td> <td>Call MCP tools</td></tr>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/models</code></td> <td><code style="color:#60a5fa;">api</code></td><td>List your models</td></tr>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/providers</code></td> <td><code style="color:#60a5fa;">api</code></td><td>List your providers</td></tr>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/rotations</code></td> <td><code style="color:#60a5fa;">api</code></td><td>List your rotations</td></tr>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/api/u/{{ session.username }}/autoselects</code></td> <td><code style="color:#60a5fa;">api</code></td><td>List your autoselects</td></tr>
<tr><td><span class="method-badge method-POST">POST</span></td><td><code>/api/u/{{ session.username }}/chat/completions</code></td><td><code style="color:#60a5fa;">api</code></td><td>Chat using your configs</td></tr>
<tr><td><span class="method-badge method-GET">GET</span></td> <td><code>/mcp/u/{{ session.username }}/tools</code></td> <td><code style="color:#a78bfa;">mcp</code></td><td>List MCP tools</td></tr>
<tr><td><span class="method-badge method-POST">POST</span></td><td><code>/mcp/u/{{ session.username }}/tools/call</code></td> <td><code style="color:#a78bfa;">mcp</code></td><td>Call MCP tools</td></tr>
</tbody>
</table>
......@@ -353,7 +379,10 @@ function renderTokens() {
return;
}
list.innerHTML = tokens.map(t => `
list.innerHTML = tokens.map(t => {
const scopeLabel = {api: 'API', mcp: 'MCP', both: 'API+MCP'}[t.scope || 'api'] || 'API';
const scopeColor = {api: '#60a5fa', mcp: '#a78bfa', both: '#4ade80'}[t.scope || 'api'] || '#60a5fa';
return `
<div class="token-card" id="token-${t.id}">
<div class="token-card-top">
<div>
......@@ -361,6 +390,7 @@ function renderTokens() {
<div class="token-meta">
<span><i class="fas fa-calendar-plus"></i> Created ${formatDate(t.created_at)}</span>
${t.last_used ? `<span><i class="fas fa-clock"></i> Last used ${formatDate(t.last_used)}</span>` : ''}
<span style="color:${scopeColor};"><i class="fas fa-shield-halved"></i> ${scopeLabel}</span>
</div>
</div>
<div class="token-actions">
......@@ -377,7 +407,7 @@ function renderTokens() {
<button class="copy-btn" onclick="copyPartialToken(this, ${JSON.stringify(t.token)})">Copy full</button>
</div>
</div>
`).join('');
`}).join('');
}
function escHtml(s) {
......@@ -404,8 +434,10 @@ function toggleCreateForm() {
function submitCreateToken() {
const desc = document.getElementById('tokenDescription').value.trim();
const scope = document.querySelector('input[name="tokenScope"]:checked')?.value || 'api';
const formData = new FormData();
if (desc) formData.append('description', desc);
formData.append('scope', scope);
fetch('{{ url_for(request, "/dashboard/user/tokens") }}', {
method: 'POST',
......@@ -425,7 +457,8 @@ function submitCreateToken() {
description: desc || null,
created_at: new Date().toISOString(),
last_used: null,
is_active: true
is_active: true,
scope: data.scope || scope
});
renderTokens();
})
......
......@@ -350,7 +350,7 @@ document.addEventListener('DOMContentLoaded', function () {
threshold_amount: parseFloat(document.getElementById('auto-topup-threshold').value) || null,
payment_method: 'stripe'
};
fetch('/api/wallet/auto-topup', {
fetch('/dashboard/wallet/auto-topup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
......
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