Better enabled functionalities evidenced

parent bbbfbef8
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
![CoderAI](CoderAI.gif) ![CoderAI](CoderAI.gif)
An OpenAI-compatible API server with web administration dashboard, supporting multiple GPU backends: NVIDIA (CUDA), AMD (Vulkan), and Intel (Vulkan). Configuration-driven architecture with per-model settings and full multi-modal support. An OpenAI-compatible API server to run models on your local GPU with web administration dashboard, supporting multiple GPU backends: NVIDIA (CUDA), AMD (Vulkan), and Intel (Vulkan). Configuration-driven architecture with per-model settings and full multi-modal support.
## Features ## Features
......
...@@ -357,6 +357,17 @@ class SessionManager: ...@@ -357,6 +357,17 @@ class SessionManager:
self._save_auth_data(auth_data) self._save_auth_data(auth_data)
return True return True
def verify_token(self, token: str) -> bool:
"""Verify an API bearer token."""
if not token:
return False
auth_data = self._load_auth_data()
for t in auth_data.get("tokens", []):
stored = t.get("token", "")
if stored and hmac.compare_digest(stored, token):
return True
return False
def delete_user(self, username: str) -> bool: def delete_user(self, username: str) -> bool:
"""Delete a user. """Delete a user.
......
...@@ -73,6 +73,7 @@ ...@@ -73,6 +73,7 @@
font-size:9px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; font-size:9px; font-weight:700; letter-spacing:.04em; text-transform:uppercase;
background:var(--surface-3,#333); color:var(--text-2); background:var(--surface-3,#333); color:var(--text-2);
} }
.t1btn.state-ready .tab-status, .t2btn.state-ready .tab-status { background:#0d2e18; color:#4ade80; }
.t1btn.state-partial .tab-status, .t2btn.state-partial .tab-status { background:#3a2510; color:#f0c060; } .t1btn.state-partial .tab-status, .t2btn.state-partial .tab-status { background:#3a2510; color:#f0c060; }
.t1btn.state-unavailable .tab-status, .t2btn.state-unavailable .tab-status { background:var(--surface-2); color:var(--text-3); } .t1btn.state-unavailable .tab-status, .t2btn.state-unavailable .tab-status { background:var(--surface-2); color:var(--text-3); }
.t1btn.active .tab-status { background:rgba(255,255,255,.18); color:#fff; } .t1btn.active .tab-status { background:rgba(255,255,255,.18); color:#fff; }
...@@ -173,6 +174,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -173,6 +174,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
font-size:10px; border-radius:999px; padding:.16rem .45rem; font-size:10px; border-radius:999px; padding:.16rem .45rem;
background:var(--surface-2); color:var(--text-2); border:1px solid var(--border); background:var(--surface-2); color:var(--text-2); border:1px solid var(--border);
} }
.cap-chip.ok { background:#0d2e18; color:#4ade80; border-color:transparent; }
.cap-chip.warn { background:#3a2510; color:#f0c060; border-color:transparent; } .cap-chip.warn { background:#3a2510; color:#f0c060; border-color:transparent; }
.cap-chip.dim { opacity:.72; } .cap-chip.dim { opacity:.72; }
.cap-missing, .cap-note { font-size:12px; color:var(--text-2); } .cap-missing, .cap-note { font-size:12px; color:var(--text-2); }
...@@ -1494,7 +1496,7 @@ const SUB_PANEL_ALIAS = { ...@@ -1494,7 +1496,7 @@ const SUB_PANEL_ALIAS = {
// Video models also enable all video sub-tabs // Video models also enable all video sub-tabs
const VIDEO_EXTRA_SUBS = ['vid-ti2v', 'vid-dub', 'vid-v2v', 'vid-sub', 'vid-interp', 'vid-up', 'vid-faceswap', 'vid-outfit']; const VIDEO_EXTRA_SUBS = ['vid-ti2v', 'vid-dub', 'vid-v2v', 'vid-sub', 'vid-interp', 'vid-up', 'vid-faceswap', 'vid-outfit'];
const TAB_STATE = { const TAB_STATE = {
available: { label:'', className:'' }, available: { label:'Ready', className:'state-ready' },
partial: { label:'Partial', className:'state-partial' }, partial: { label:'Partial', className:'state-partial' },
unavailable: { label:'Unavailable', className:'state-unavailable' }, unavailable: { label:'Unavailable', className:'state-unavailable' },
}; };
...@@ -1680,7 +1682,7 @@ function evaluateCategoryState(cat, subStates, caps, type) { ...@@ -1680,7 +1682,7 @@ function evaluateCategoryState(cat, subStates, caps, type) {
function setTabVisualState(btn, state) { function setTabVisualState(btn, state) {
if (!btn) return; if (!btn) return;
btn.classList.remove('state-partial', 'state-unavailable'); btn.classList.remove('state-ready', 'state-partial', 'state-unavailable');
const def = TAB_STATE[state] || TAB_STATE.unavailable; const def = TAB_STATE[state] || TAB_STATE.unavailable;
if (def.className) btn.classList.add(def.className); if (def.className) btn.classList.add(def.className);
const badge = btn.querySelector('.tab-status'); const badge = btn.querySelector('.tab-status');
...@@ -1736,8 +1738,8 @@ function renderCapabilityCard(sub) { ...@@ -1736,8 +1738,8 @@ function renderCapabilityCard(sub) {
shell.classList.remove('state-partial', 'state-unavailable'); shell.classList.remove('state-partial', 'state-unavailable');
if (details.availability === 'partial') shell.classList.add('state-partial'); if (details.availability === 'partial') shell.classList.add('state-partial');
if (details.availability === 'unavailable') shell.classList.add('state-unavailable'); if (details.availability === 'unavailable') shell.classList.add('state-unavailable');
const availabilityLabel = details.availability === 'available' ? 'Available' : details.availability === 'partial' ? 'Partial' : 'Unavailable'; const availabilityLabel = details.availability === 'available' ? 'Ready' : details.availability === 'partial' ? 'Partial' : 'Unavailable';
const availabilityClass = details.availability === 'available' ? '' : details.availability === 'partial' ? ' warn' : ' dim'; const availabilityClass = details.availability === 'available' ? ' ok' : details.availability === 'partial' ? ' warn' : ' dim';
const missingBits = []; const missingBits = [];
if (details.missingRequired.length) missingBits.push(`<div class="cap-missing"><strong>Missing required:</strong> ${details.missingRequired.join(', ')}</div>`); if (details.missingRequired.length) missingBits.push(`<div class="cap-missing"><strong>Missing required:</strong> ${details.missingRequired.join(', ')}</div>`);
if (details.missingOptional.length) missingBits.push(`<div class="cap-missing"><strong>Limited without:</strong> ${details.missingOptional.join(', ')}</div>`); if (details.missingOptional.length) missingBits.push(`<div class="cap-missing"><strong>Limited without:</strong> ${details.missingOptional.join(', ')}</div>`);
...@@ -2138,15 +2140,18 @@ function buildDubPreviewData() { ...@@ -2138,15 +2140,18 @@ function buildDubPreviewData() {
} }
function buildCodeSnippet(kind, preview) { function buildCodeSnippet(kind, preview) {
const origin = window.location.hostname === '0.0.0.0'
? window.location.origin.replace('0.0.0.0', '127.0.0.1')
: window.location.origin;
if (kind === 'curl') { if (kind === 'curl') {
return `curl -X POST http://localhost:8000${preview.endpoint} \\ return `curl -X POST ${origin}${preview.endpoint} \\
-H "Content-Type: application/json" \\ -H "Content-Type: application/json" \\
-d '${preview.json.replace(/'/g, "'\\''")}'`; -d '${preview.json.replace(/'/g, "'\\''")}'`;
} }
if (kind === 'python') { if (kind === 'python') {
return `import requests\n\npayload = ${preview.json}\nresponse = requests.post(\n "http://localhost:8000${preview.endpoint}",\n json=payload,\n timeout=300,\n)\nprint(response.json())`; return `import requests\n\npayload = ${preview.json}\nresponse = requests.post(\n "${origin}${preview.endpoint}",\n json=payload,\n timeout=300,\n)\nprint(response.json())`;
} }
return `const payload = ${preview.json};\nconst response = await fetch("${preview.endpoint}", {\n method: "POST",\n headers: { "Content-Type": "application/json" },\n body: JSON.stringify(payload),\n});\nconst data = await response.json();\nconsole.log(data);`; return `const payload = ${preview.json};\nconst response = await fetch("${origin}${preview.endpoint}", {\n method: "POST",\n headers: { "Content-Type": "application/json" },\n body: JSON.stringify(payload),\n});\nconst data = await response.json();\nconsole.log(data);`;
} }
function renderRequestPreview(panel, config) { function renderRequestPreview(panel, config) {
......
...@@ -105,9 +105,10 @@ from codai.admin.routes import router as admin_router ...@@ -105,9 +105,10 @@ from codai.admin.routes import router as admin_router
# Import and add middleware # Import and add middleware
from codai.api.log import log_requests from codai.api.log import log_requests
from codai.api.ratelimit import RateLimitMiddleware from codai.api.ratelimit import RateLimitMiddleware, BearerAuthMiddleware
app.middleware("http")(log_requests) app.middleware("http")(log_requests)
app.add_middleware(RateLimitMiddleware) app.add_middleware(RateLimitMiddleware)
app.add_middleware(BearerAuthMiddleware)
# Mount static files for admin dashboard # Mount static files for admin dashboard
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
......
...@@ -31,6 +31,45 @@ from fastapi.responses import JSONResponse ...@@ -31,6 +31,45 @@ from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
class BearerAuthMiddleware(BaseHTTPMiddleware):
"""Reject /v1/ API requests that lack a valid Bearer token or active web session."""
async def dispatch(self, request: Request, call_next):
path = request.url.path
if not path.startswith("/v1/"):
return await call_next(request)
from codai.admin import routes as _admin_routes
sm = _admin_routes.session_manager
if sm is None:
return await call_next(request)
# Accept a valid Bearer token
auth_header = request.headers.get("authorization", "")
if auth_header.lower().startswith("bearer "):
token = auth_header[7:].strip()
if sm.verify_token(token):
return await call_next(request)
# Accept a valid web session cookie (logged-in browser user)
cookie = request.cookies.get("session", "")
if cookie.endswith(".MUST_CHANGE"):
cookie = cookie[:-12]
if cookie and sm.validate_session(cookie):
return await call_next(request)
return JSONResponse(
status_code=401,
content={
"error": {
"message": "Invalid API key. Provide a valid Bearer token.",
"type": "invalid_request_error",
"code": "invalid_api_key",
}
},
)
# Per-route-prefix defaults: (max_requests, window_seconds) # Per-route-prefix defaults: (max_requests, window_seconds)
_DEFAULT_LIMITS: Dict[str, Tuple[int, int]] = { _DEFAULT_LIMITS: Dict[str, Tuple[int, int]] = {
"/v1/chat/completions": (60, 60), "/v1/chat/completions": (60, 60),
......
...@@ -239,6 +239,8 @@ def choose_mode_interactively() -> str: ...@@ -239,6 +239,8 @@ def choose_mode_interactively() -> str:
for idx, mode in enumerate(MODES, start=1): for idx, mode in enumerate(MODES, start=1):
print(f"{idx}. {mode}") print(f"{idx}. {mode}")
raw = input("Choose mode: ").strip() raw = input("Choose mode: ").strip()
if raw in MODES:
return raw
selected = int(raw) selected = int(raw)
if selected < 1 or selected > len(MODES): if selected < 1 or selected > len(MODES):
raise ValueError(f"Invalid mode selection: {raw}") raise ValueError(f"Invalid mode selection: {raw}")
......
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